provide-uterm 0.5.1__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.
- provide_uterm-0.5.1/MANIFEST.in +6 -0
- provide_uterm-0.5.1/PKG-INFO +74 -0
- provide_uterm-0.5.1/README.md +41 -0
- provide_uterm-0.5.1/VERSION +1 -0
- provide_uterm-0.5.1/pyproject.toml +178 -0
- provide_uterm-0.5.1/setup.cfg +4 -0
- provide_uterm-0.5.1/src/provide/uterm/__init__.py +172 -0
- provide_uterm-0.5.1/src/provide/uterm/_ansi_dialects.py +205 -0
- provide_uterm-0.5.1/src/provide/uterm/ansi.py +346 -0
- provide_uterm-0.5.1/src/provide/uterm/api_routes.py +398 -0
- provide_uterm-0.5.1/src/provide/uterm/auth.py +344 -0
- provide_uterm-0.5.1/src/provide/uterm/bridge/__init__.py +19 -0
- provide_uterm-0.5.1/src/provide/uterm/bridge/base.py +172 -0
- provide_uterm-0.5.1/src/provide/uterm/bridge/contracts.py +262 -0
- provide_uterm-0.5.1/src/provide/uterm/bridge/coordinator.py +138 -0
- provide_uterm-0.5.1/src/provide/uterm/bridge/policy.py +85 -0
- provide_uterm-0.5.1/src/provide/uterm/bridge/schemas.py +474 -0
- provide_uterm-0.5.1/src/provide/uterm/cf_ws_transport.py +246 -0
- provide_uterm-0.5.1/src/provide/uterm/channels.py +126 -0
- provide_uterm-0.5.1/src/provide/uterm/colors/__init__.py +38 -0
- provide_uterm-0.5.1/src/provide/uterm/colors/downgrade.py +51 -0
- provide_uterm-0.5.1/src/provide/uterm/colors/mode.py +49 -0
- provide_uterm-0.5.1/src/provide/uterm/colors/rgb.py +99 -0
- provide_uterm-0.5.1/src/provide/uterm/colors/sgr.py +81 -0
- provide_uterm-0.5.1/src/provide/uterm/control/__init__.py +5 -0
- provide_uterm-0.5.1/src/provide/uterm/control/channel/__init__.py +6 -0
- provide_uterm-0.5.1/src/provide/uterm/control/channel/browser.py +9 -0
- provide_uterm-0.5.1/src/provide/uterm/control/channel/protocol.py +23 -0
- provide_uterm-0.5.1/src/provide/uterm/control/channel/worker.py +9 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/__init__.py +15 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/approval/__init__.py +6 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/approval/store.py +18 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/approval/types.py +22 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/bootstrap.py +55 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/capability.py +16 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/errors.py +28 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/graphical_target/__init__.py +29 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/graphical_target/store.py +28 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/graphical_target/types.py +43 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/lease/__init__.py +6 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/lease/store.py +18 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/lease/types.py +17 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/memory/__init__.py +20 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/memory/approval_store.py +29 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/memory/engine.py +116 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/memory/graphical_target_store.py +31 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/memory/lease_store.py +26 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/memory/session_store.py +30 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/memory/token_store.py +46 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/memory/transaction.py +140 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/session/__init__.py +6 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/session/store.py +18 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/session/types.py +24 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/__init__.py +20 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/approval_store.py +77 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/connection.py +50 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/engine.py +170 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/graphical_target_store.py +161 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/lease_store.py +72 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/migration.py +49 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/schema/__init__.py +7 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/schema/v0001_initial.py +64 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/schema/v0002_audit_head.py +22 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/schema/v0003_graphical_targets.py +53 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/session_store.py +83 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/token_store.py +143 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/transaction.py +39 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/token/__init__.py +6 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/token/store.py +24 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/token/types.py +28 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/transaction/__init__.py +5 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/transaction/types.py +13 -0
- provide_uterm-0.5.1/src/provide/uterm/control/plane/types.py +32 -0
- provide_uterm-0.5.1/src/provide/uterm/control_channel.py +471 -0
- provide_uterm-0.5.1/src/provide/uterm/control_channel_builders.py +198 -0
- provide_uterm-0.5.1/src/provide/uterm/control_channel_patterns.py +163 -0
- provide_uterm-0.5.1/src/provide/uterm/deckmux/__init__.py +43 -0
- provide_uterm-0.5.1/src/provide/uterm/deckmux/_edge.py +36 -0
- provide_uterm-0.5.1/src/provide/uterm/deckmux/_hub_mixin.py +138 -0
- provide_uterm-0.5.1/src/provide/uterm/deckmux/_identity.py +219 -0
- provide_uterm-0.5.1/src/provide/uterm/deckmux/_names.py +126 -0
- provide_uterm-0.5.1/src/provide/uterm/deckmux/_presence.py +172 -0
- provide_uterm-0.5.1/src/provide/uterm/deckmux/_protocol.py +92 -0
- provide_uterm-0.5.1/src/provide/uterm/deckmux/_service.py +266 -0
- provide_uterm-0.5.1/src/provide/uterm/deckmux/_transfer.py +112 -0
- provide_uterm-0.5.1/src/provide/uterm/deckmux/py.typed +0 -0
- provide_uterm-0.5.1/src/provide/uterm/defaults.py +48 -0
- provide_uterm-0.5.1/src/provide/uterm/detection/__init__.py +38 -0
- provide_uterm-0.5.1/src/provide/uterm/detection/buffer.py +106 -0
- provide_uterm-0.5.1/src/provide/uterm/detection/detector.py +455 -0
- provide_uterm-0.5.1/src/provide/uterm/detection/detector_compile.py +140 -0
- provide_uterm-0.5.1/src/provide/uterm/detection/engine.py +257 -0
- provide_uterm-0.5.1/src/provide/uterm/detection/extractor.py +219 -0
- provide_uterm-0.5.1/src/provide/uterm/detection/flow.py +151 -0
- provide_uterm-0.5.1/src/provide/uterm/detection/input_type.py +68 -0
- provide_uterm-0.5.1/src/provide/uterm/detection/loader.py +31 -0
- provide_uterm-0.5.1/src/provide/uterm/detection/models.py +59 -0
- provide_uterm-0.5.1/src/provide/uterm/detection/rules.py +166 -0
- provide_uterm-0.5.1/src/provide/uterm/detection/saver.py +202 -0
- provide_uterm-0.5.1/src/provide/uterm/embed/__init__.py +55 -0
- provide_uterm-0.5.1/src/provide/uterm/embed/hub.py +59 -0
- provide_uterm-0.5.1/src/provide/uterm/embed/session.py +413 -0
- provide_uterm-0.5.1/src/provide/uterm/embed/telnet_upstream.py +150 -0
- provide_uterm-0.5.1/src/provide/uterm/embed/types.py +178 -0
- provide_uterm-0.5.1/src/provide/uterm/emulator.py +287 -0
- provide_uterm-0.5.1/src/provide/uterm/expect.py +89 -0
- provide_uterm-0.5.1/src/provide/uterm/file_io.py +99 -0
- provide_uterm-0.5.1/src/provide/uterm/filters.py +101 -0
- provide_uterm-0.5.1/src/provide/uterm/frames.py +71 -0
- provide_uterm-0.5.1/src/provide/uterm/io.py +268 -0
- provide_uterm-0.5.1/src/provide/uterm/line_editor.py +220 -0
- provide_uterm-0.5.1/src/provide/uterm/protocols.py +29 -0
- provide_uterm-0.5.1/src/provide/uterm/py.typed +0 -0
- provide_uterm-0.5.1/src/provide/uterm/recording.py +374 -0
- provide_uterm-0.5.1/src/provide/uterm/redaction.py +35 -0
- provide_uterm-0.5.1/src/provide/uterm/render/__init__.py +49 -0
- provide_uterm-0.5.1/src/provide/uterm/render/buffer.py +230 -0
- provide_uterm-0.5.1/src/provide/uterm/render/image.py +124 -0
- provide_uterm-0.5.1/src/provide/uterm/render/palette.py +97 -0
- provide_uterm-0.5.1/src/provide/uterm/render/py.typed +0 -0
- provide_uterm-0.5.1/src/provide/uterm/render/segments.py +142 -0
- provide_uterm-0.5.1/src/provide/uterm/render/sgr.py +46 -0
- provide_uterm-0.5.1/src/provide/uterm/replay/__init__.py +13 -0
- provide_uterm-0.5.1/src/provide/uterm/replay/raw.py +35 -0
- provide_uterm-0.5.1/src/provide/uterm/replay/viewer.py +83 -0
- provide_uterm-0.5.1/src/provide/uterm/sanitizer.py +57 -0
- provide_uterm-0.5.1/src/provide/uterm/screen.py +200 -0
- provide_uterm-0.5.1/src/provide/uterm/session_logger.py +313 -0
- provide_uterm-0.5.1/src/provide/uterm/shell/__main__.py +65 -0
- provide_uterm-0.5.1/src/provide/uterm/shell/_output.py +79 -0
- provide_uterm-0.5.1/src/provide/uterm/shell/_render.py +59 -0
- provide_uterm-0.5.1/src/provide/uterm/shell/_repl.py +137 -0
- provide_uterm-0.5.1/src/provide/uterm/shell/_sandbox.py +173 -0
- provide_uterm-0.5.1/src/provide/uterm/shell/commands/__init__.py +40 -0
- provide_uterm-0.5.1/src/provide/uterm/shell/commands/cast.py +105 -0
- provide_uterm-0.5.1/src/provide/uterm/shell/commands/dispatcher.py +147 -0
- provide_uterm-0.5.1/src/provide/uterm/shell/commands/fetch.py +64 -0
- provide_uterm-0.5.1/src/provide/uterm/shell/commands/help.py +74 -0
- provide_uterm-0.5.1/src/provide/uterm/shell/commands/kv.py +131 -0
- provide_uterm-0.5.1/src/provide/uterm/shell/commands/py.py +23 -0
- provide_uterm-0.5.1/src/provide/uterm/shell/commands/render.py +99 -0
- provide_uterm-0.5.1/src/provide/uterm/shell/commands/storage.py +54 -0
- provide_uterm-0.5.1/src/provide/uterm/shell/commands/types.py +18 -0
- provide_uterm-0.5.1/src/provide/uterm/shell/terminal/__init__.py +7 -0
- provide_uterm-0.5.1/src/provide/uterm/shell/terminal/_connector.py +223 -0
- provide_uterm-0.5.1/src/provide/uterm/shell/terminal/_output.py +41 -0
- provide_uterm-0.5.1/src/provide/uterm/telnet_session.py +148 -0
- provide_uterm-0.5.1/src/provide/uterm/terminal_frames.py +32 -0
- provide_uterm-0.5.1/src/provide/uterm/transport_session.py +554 -0
- provide_uterm-0.5.1/src/provide/uterm/vnc/__init__.py +23 -0
- provide_uterm-0.5.1/src/provide/uterm/vnc/human_relay.py +192 -0
- provide_uterm-0.5.1/src/provide/uterm/vnc/rfb_filter.py +149 -0
- provide_uterm-0.5.1/src/provide/uterm/ws_bytes.py +45 -0
- provide_uterm-0.5.1/src/provide/uterm/ws_session.py +144 -0
- provide_uterm-0.5.1/src/provide_uterm.egg-info/PKG-INFO +74 -0
- provide_uterm-0.5.1/src/provide_uterm.egg-info/SOURCES.txt +196 -0
- provide_uterm-0.5.1/src/provide_uterm.egg-info/dependency_links.txt +1 -0
- provide_uterm-0.5.1/src/provide_uterm.egg-info/requires.txt +17 -0
- provide_uterm-0.5.1/src/provide_uterm.egg-info/top_level.txt +1 -0
- provide_uterm-0.5.1/tests/test_ansi_render.py +272 -0
- provide_uterm-0.5.1/tests/test_api_routes.py +302 -0
- provide_uterm-0.5.1/tests/test_auth.py +227 -0
- provide_uterm-0.5.1/tests/test_auth_authorized_keys.py +205 -0
- provide_uterm-0.5.1/tests/test_auth_kill.py +42 -0
- provide_uterm-0.5.1/tests/test_auth_mutmut.py +408 -0
- provide_uterm-0.5.1/tests/test_auth_parsers.py +266 -0
- provide_uterm-0.5.1/tests/test_auth_resolver.py +160 -0
- provide_uterm-0.5.1/tests/test_cf_ws_transport.py +460 -0
- provide_uterm-0.5.1/tests/test_colors.py +290 -0
- provide_uterm-0.5.1/tests/test_control_channel_builders.py +432 -0
- provide_uterm-0.5.1/tests/test_control_channel_hypothesis.py +142 -0
- provide_uterm-0.5.1/tests/test_control_channel_ordering.py +333 -0
- provide_uterm-0.5.1/tests/test_control_channel_patterns.py +328 -0
- provide_uterm-0.5.1/tests/test_control_frame_public_api.py +116 -0
- provide_uterm-0.5.1/tests/test_detection_hypothesis.py +108 -0
- provide_uterm-0.5.1/tests/test_embed_session.py +473 -0
- provide_uterm-0.5.1/tests/test_embed_telnet.py +135 -0
- provide_uterm-0.5.1/tests/test_expect.py +122 -0
- provide_uterm-0.5.1/tests/test_file_io_secure.py +54 -0
- provide_uterm-0.5.1/tests/test_frame_builders.py +58 -0
- provide_uterm-0.5.1/tests/test_frontend_sri.py +117 -0
- provide_uterm-0.5.1/tests/test_high_roi_mutmut.py +440 -0
- provide_uterm-0.5.1/tests/test_io.py +405 -0
- provide_uterm-0.5.1/tests/test_io_mutations.py +295 -0
- provide_uterm-0.5.1/tests/test_io_mutations_2.py +482 -0
- provide_uterm-0.5.1/tests/test_io_mutations_3.py +362 -0
- provide_uterm-0.5.1/tests/test_io_mutmut.py +330 -0
- provide_uterm-0.5.1/tests/test_mutant_kills_emulator.py +324 -0
- provide_uterm-0.5.1/tests/test_package_lazy_attr.py +31 -0
- provide_uterm-0.5.1/tests/test_protocol_negotiation.py +69 -0
- provide_uterm-0.5.1/tests/test_reader_loop_logger_trace.py +89 -0
- provide_uterm-0.5.1/tests/test_recording_permissions.py +221 -0
- provide_uterm-0.5.1/tests/test_redaction.py +25 -0
- provide_uterm-0.5.1/tests/test_sanitizer.py +31 -0
- provide_uterm-0.5.1/tests/test_typed_channels.py +152 -0
- provide_uterm-0.5.1/tests/test_vnc_human_relay.py +436 -0
- provide_uterm-0.5.1/tests/test_vnc_human_relay_driver.py +176 -0
- provide_uterm-0.5.1/tests/test_vnc_rfb_filter.py +240 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: provide-uterm
|
|
3
|
+
Version: 0.5.1
|
|
4
|
+
Summary: Shared terminal I/O primitives for the provide ecosystem
|
|
5
|
+
Author: provide.io llc
|
|
6
|
+
License-Expression: AGPL-3.0-or-later
|
|
7
|
+
Project-URL: Homepage, https://github.com/provide-io/provide-uterm
|
|
8
|
+
Project-URL: Repository, https://github.com/provide-io/provide-uterm
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: System :: Networking
|
|
16
|
+
Classifier: Topic :: Terminals
|
|
17
|
+
Classifier: Framework :: FastAPI
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: provide-telemetry>=0.7
|
|
21
|
+
Requires-Dist: aiosqlite>=0.22
|
|
22
|
+
Requires-Dist: pydantic>=2.0
|
|
23
|
+
Provides-Extra: emulator
|
|
24
|
+
Requires-Dist: pyte>=0.8; extra == "emulator"
|
|
25
|
+
Provides-Extra: ssh
|
|
26
|
+
Requires-Dist: asyncssh>=2.14; extra == "ssh"
|
|
27
|
+
Provides-Extra: client
|
|
28
|
+
Requires-Dist: httpx2>=2.10; extra == "client"
|
|
29
|
+
Provides-Extra: all
|
|
30
|
+
Requires-Dist: pyte>=0.8; extra == "all"
|
|
31
|
+
Requires-Dist: asyncssh>=2.14; extra == "all"
|
|
32
|
+
Requires-Dist: httpx2>=2.10; extra == "all"
|
|
33
|
+
|
|
34
|
+
# provide-uterm
|
|
35
|
+
|
|
36
|
+
Core shared package for the [provide-uterm](../../README.md) platform. It
|
|
37
|
+
contains terminal I/O primitives, screen parsing, replay helpers, detection,
|
|
38
|
+
rendering, and small operator-facing utilities used by the split companion
|
|
39
|
+
packages.
|
|
40
|
+
|
|
41
|
+
## What's in this package
|
|
42
|
+
|
|
43
|
+
| Module | Purpose |
|
|
44
|
+
|--------|---------|
|
|
45
|
+
| `ansi.py`, `colors/`, `render/` | ANSI processing, color transforms, render helpers |
|
|
46
|
+
| `screen.py`, `detection/` | Screen cleanup, prompt parsing, semantic extraction |
|
|
47
|
+
| `telnet_session.py`, `line_editor.py` | Terminal session and local editing helpers |
|
|
48
|
+
| `replay/`, `session_logger.py` | Recording and replay utilities |
|
|
49
|
+
| Root package exports | Curated shared primitives such as `strip_ansi`, `normalize_colors`, `LineEditor`, and file loaders |
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install 'provide-uterm[all]'
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
See the [main README](../../README.md) for extras and quick start guides.
|
|
58
|
+
|
|
59
|
+
## Split Packages
|
|
60
|
+
|
|
61
|
+
| Package | Owns |
|
|
62
|
+
|---------|--------|
|
|
63
|
+
| `provide-uterm` | Shared terminal primitives and root facade exports, plus `provide.uterm.bridge` (HijackableMixin, contracts, schemas) |
|
|
64
|
+
| `provide-uterm-server` | `provide.uterm.server` (incl. `provide.uterm.server.bridge`), `provide.uterm.gateway`, `provide.uterm.tunnel` |
|
|
65
|
+
| `provide-uterm-platform` | `provide.uterm.manager`, `provide.uterm.pty` |
|
|
66
|
+
| `provide-uterm-client` | Client-side session control and MCP tooling |
|
|
67
|
+
|
|
68
|
+
## Related Packages
|
|
69
|
+
|
|
70
|
+
This package depends on several companion packages via workspace symlinks. See the [package ecosystem table](../../README.md#package-ecosystem) for the full list.
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
AGPL-3.0-or-later. Copyright (c) 2025-2026 provide.io llc.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# provide-uterm
|
|
2
|
+
|
|
3
|
+
Core shared package for the [provide-uterm](../../README.md) platform. It
|
|
4
|
+
contains terminal I/O primitives, screen parsing, replay helpers, detection,
|
|
5
|
+
rendering, and small operator-facing utilities used by the split companion
|
|
6
|
+
packages.
|
|
7
|
+
|
|
8
|
+
## What's in this package
|
|
9
|
+
|
|
10
|
+
| Module | Purpose |
|
|
11
|
+
|--------|---------|
|
|
12
|
+
| `ansi.py`, `colors/`, `render/` | ANSI processing, color transforms, render helpers |
|
|
13
|
+
| `screen.py`, `detection/` | Screen cleanup, prompt parsing, semantic extraction |
|
|
14
|
+
| `telnet_session.py`, `line_editor.py` | Terminal session and local editing helpers |
|
|
15
|
+
| `replay/`, `session_logger.py` | Recording and replay utilities |
|
|
16
|
+
| Root package exports | Curated shared primitives such as `strip_ansi`, `normalize_colors`, `LineEditor`, and file loaders |
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install 'provide-uterm[all]'
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
See the [main README](../../README.md) for extras and quick start guides.
|
|
25
|
+
|
|
26
|
+
## Split Packages
|
|
27
|
+
|
|
28
|
+
| Package | Owns |
|
|
29
|
+
|---------|--------|
|
|
30
|
+
| `provide-uterm` | Shared terminal primitives and root facade exports, plus `provide.uterm.bridge` (HijackableMixin, contracts, schemas) |
|
|
31
|
+
| `provide-uterm-server` | `provide.uterm.server` (incl. `provide.uterm.server.bridge`), `provide.uterm.gateway`, `provide.uterm.tunnel` |
|
|
32
|
+
| `provide-uterm-platform` | `provide.uterm.manager`, `provide.uterm.pty` |
|
|
33
|
+
| `provide-uterm-client` | Client-side session control and MCP tooling |
|
|
34
|
+
|
|
35
|
+
## Related Packages
|
|
36
|
+
|
|
37
|
+
This package depends on several companion packages via workspace symlinks. See the [package ecosystem table](../../README.md#package-ecosystem) for the full list.
|
|
38
|
+
|
|
39
|
+
## License
|
|
40
|
+
|
|
41
|
+
AGPL-3.0-or-later. Copyright (c) 2025-2026 provide.io llc.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.5.1
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
[build-system]
|
|
7
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
8
|
+
build-backend = "setuptools.build_meta"
|
|
9
|
+
|
|
10
|
+
[project]
|
|
11
|
+
name = "provide-uterm"
|
|
12
|
+
dynamic = ["version"]
|
|
13
|
+
description = "Shared terminal I/O primitives for the provide ecosystem"
|
|
14
|
+
readme = "README.md"
|
|
15
|
+
requires-python = ">=3.11"
|
|
16
|
+
license = "AGPL-3.0-or-later"
|
|
17
|
+
authors = [
|
|
18
|
+
{name = "provide.io llc"},
|
|
19
|
+
]
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Development Status :: 4 - Beta",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: System :: Networking",
|
|
28
|
+
"Topic :: Terminals",
|
|
29
|
+
"Framework :: FastAPI",
|
|
30
|
+
]
|
|
31
|
+
# pydantic is a RUNTIME dependency, not a dev one: bridge/schemas.py defines the
|
|
32
|
+
# wire-format frame models, and the eager provide/uterm/__init__.py pulls it in
|
|
33
|
+
# on import. It was only ever declared in the dev extra and arrived transitively
|
|
34
|
+
# everywhere else, so the omission stayed invisible until a lock refresh stopped
|
|
35
|
+
# supplying it and CI's isolated `--package` env failed to import pydantic.
|
|
36
|
+
dependencies = ["provide-telemetry>=0.7", "aiosqlite>=0.22", "pydantic>=2.0"]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/provide-io/provide-uterm"
|
|
40
|
+
Repository = "https://github.com/provide-io/provide-uterm"
|
|
41
|
+
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
emulator = ["pyte>=0.8"]
|
|
44
|
+
ssh = ["asyncssh>=2.14"]
|
|
45
|
+
client = ["httpx2>=2.10"]
|
|
46
|
+
all = ["pyte>=0.8", "asyncssh>=2.14", "httpx2>=2.10"]
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.dynamic]
|
|
49
|
+
version = {file = "VERSION"}
|
|
50
|
+
|
|
51
|
+
[tool.setuptools.packages.find]
|
|
52
|
+
where = ["src"]
|
|
53
|
+
|
|
54
|
+
[tool.setuptools.package-data]
|
|
55
|
+
"provide.uterm" = [
|
|
56
|
+
"py.typed",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[dependency-groups]
|
|
60
|
+
dev = [
|
|
61
|
+
"asyncssh>=2.22.0",
|
|
62
|
+
"bandit>=1.9",
|
|
63
|
+
"codespell>=2.4.1",
|
|
64
|
+
"detect-secrets>=1.5.0",
|
|
65
|
+
"fastapi>=0.110",
|
|
66
|
+
"httpx2>=2.10",
|
|
67
|
+
"hypothesis>=6.151.9",
|
|
68
|
+
"memray>=1.0; sys_platform != 'win32'",
|
|
69
|
+
"mutmut>=3.3.1",
|
|
70
|
+
"mypy>=1.19",
|
|
71
|
+
"pip-audit>=2.7.0",
|
|
72
|
+
"pip-licenses>=5.5.0",
|
|
73
|
+
"pre-commit>=4.0",
|
|
74
|
+
"pydantic>=2.0",
|
|
75
|
+
"pyjwt>=2.9",
|
|
76
|
+
"pyte>=0.8.2",
|
|
77
|
+
"pytest>=9.0",
|
|
78
|
+
"pytest-asyncio>=1.3",
|
|
79
|
+
"pytest-cov>=7.0",
|
|
80
|
+
"pytest-playwright>=0.7.2",
|
|
81
|
+
"pytest-randomly>=3.15.0",
|
|
82
|
+
"pytest-timeout>=2.4.0",
|
|
83
|
+
"pytest-xdist>=3.8",
|
|
84
|
+
"reuse>=6.2.0",
|
|
85
|
+
"ruff>=0.15",
|
|
86
|
+
"ty>=0.0.17",
|
|
87
|
+
"uv>=0.10.7",
|
|
88
|
+
"uvicorn>=0.24",
|
|
89
|
+
"vulture>=2.14",
|
|
90
|
+
"websockets>=16.0",
|
|
91
|
+
"pyyaml>=6.0",
|
|
92
|
+
"xenon>=0.9.3",
|
|
93
|
+
"mcp>=2.0.0",
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
[tool.pytest.ini_options]
|
|
97
|
+
consider_namespace_packages = true
|
|
98
|
+
asyncio_mode = "auto"
|
|
99
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
100
|
+
testpaths = ["tests"]
|
|
101
|
+
filterwarnings = [
|
|
102
|
+
"ignore:websockets\\.legacy is deprecated; see .*:DeprecationWarning:websockets\\.legacy",
|
|
103
|
+
"ignore:websockets\\.server\\.WebSocketServerProtocol is deprecated:DeprecationWarning:uvicorn\\.protocols\\.websockets\\.websockets_impl",
|
|
104
|
+
"ignore:remove second argument of ws_handler:DeprecationWarning:websockets\\.legacy\\.server",
|
|
105
|
+
]
|
|
106
|
+
markers = [
|
|
107
|
+
"playwright: browser-based UI tests (deselected by default, run with -m playwright)",
|
|
108
|
+
"mutant: mutation-testing and mutmut-focused tests (deselected by default)",
|
|
109
|
+
"memray: memory profiling and allocation stress tests (deselected by default, run with -m memray)",
|
|
110
|
+
"slow: slow tests >10s (deselected by default)",
|
|
111
|
+
"docker: Docker-based E2E tests requiring a running Docker daemon (deselected by default, run with -m docker)",
|
|
112
|
+
"unit: fast unit tests with no I/O or external dependencies",
|
|
113
|
+
]
|
|
114
|
+
addopts = [
|
|
115
|
+
"--import-mode=importlib",
|
|
116
|
+
"-v",
|
|
117
|
+
"--strict-markers",
|
|
118
|
+
"--strict-config",
|
|
119
|
+
"--tb=short",
|
|
120
|
+
"-ra",
|
|
121
|
+
"-m",
|
|
122
|
+
"not playwright and not memray and not slow and not docker",
|
|
123
|
+
# Coverage scoped to this package's source dir. An import name can't be used
|
|
124
|
+
# (`provide.uterm` is a namespace shared by every package → would pull in
|
|
125
|
+
# server/client/cloudflare/… and report ~69%). This package-relative path
|
|
126
|
+
# only resolves when pytest's CWD is the package dir, so the gate MUST be run
|
|
127
|
+
# from here — CI and scripts/run_all_tests.py invoke it via
|
|
128
|
+
# `uv run --directory packages/provide-uterm pytest tests`. Waivers below.
|
|
129
|
+
"--cov=src/provide/uterm",
|
|
130
|
+
"--cov-branch",
|
|
131
|
+
"--cov-report=term-missing",
|
|
132
|
+
"--cov-fail-under=100",
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
[tool.coverage.run]
|
|
136
|
+
# Measure the whole package source, not an enumerated module allowlist. This
|
|
137
|
+
# keeps newly-added modules in the 100% gate by default. Genuinely
|
|
138
|
+
# out-of-scope modules are listed explicitly in ``omit`` below so the waiver
|
|
139
|
+
# is visible and grep-able (CORE-cov, 2026-05-28 remediation).
|
|
140
|
+
source = ["src/provide/uterm"]
|
|
141
|
+
omit = [
|
|
142
|
+
"tests/*",
|
|
143
|
+
]
|
|
144
|
+
branch = true
|
|
145
|
+
|
|
146
|
+
[tool.coverage.report]
|
|
147
|
+
fail_under = 100
|
|
148
|
+
show_missing = true
|
|
149
|
+
skip_covered = false
|
|
150
|
+
# Only the standard explicit waiver remains. The previous broad
|
|
151
|
+
# ``elif msg_type ==`` / ``more_body`` regex escape-hatches were removed
|
|
152
|
+
# (CORE-cov): real branch tests now cover those dispatch fall-throughs, and
|
|
153
|
+
# any genuinely-unreachable branch carries a per-site ``# pragma: no cover``
|
|
154
|
+
# with an inline justification so the waiver is visible and grep-able.
|
|
155
|
+
partial_branches = [
|
|
156
|
+
"pragma: no branch",
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
[tool.mutmut]
|
|
160
|
+
max_children = 6
|
|
161
|
+
source_paths = ["src/provide/uterm/"]
|
|
162
|
+
also_copy = ["scripts/"]
|
|
163
|
+
do_not_mutate = [
|
|
164
|
+
"src/provide/uterm/frontend/",
|
|
165
|
+
"src/provide/uterm/transports/",
|
|
166
|
+
"src/provide/uterm/gateway.py",
|
|
167
|
+
"src/provide/uterm/server/connectors/",
|
|
168
|
+
]
|
|
169
|
+
pytest_add_cli_args = ["--no-cov", "--timeout=30"]
|
|
170
|
+
pytest_add_cli_args_test_selection = [
|
|
171
|
+
"tests/",
|
|
172
|
+
"tests_mutation/",
|
|
173
|
+
"-m",
|
|
174
|
+
"not playwright",
|
|
175
|
+
"--ignore=tests/example/test_server_e2e.py",
|
|
176
|
+
"--ignore=tests/bridge/test_hypothesis.py",
|
|
177
|
+
"--ignore=tests/e2e",
|
|
178
|
+
]
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
"""provide-uterm: shared terminal I/O primitives for the provide ecosystem.
|
|
7
|
+
|
|
8
|
+
Server, gateway, bridge, manager, and telnet session APIs live in split
|
|
9
|
+
packages and should be imported from their explicit submodules.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import pkgutil
|
|
15
|
+
from importlib import import_module
|
|
16
|
+
|
|
17
|
+
# Allow other installed packages to contribute sub-packages under provide.uterm
|
|
18
|
+
# (e.g. provide-uterm-cloudflare contributes provide.uterm.cloudflare).
|
|
19
|
+
__path__ = pkgutil.extend_path(__path__, __name__)
|
|
20
|
+
|
|
21
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
__version__ = version("provide-uterm")
|
|
25
|
+
except PackageNotFoundError: # pragma: no cover
|
|
26
|
+
__version__ = "0.0.0"
|
|
27
|
+
|
|
28
|
+
from provide.uterm.terminal_frames import TerminalFrame, TerminalFrameDisconnectedError
|
|
29
|
+
|
|
30
|
+
from provide.uterm.ansi import (
|
|
31
|
+
BOLD,
|
|
32
|
+
CLEAR_SCREEN,
|
|
33
|
+
DEFAULT_PALETTE,
|
|
34
|
+
DEFAULT_RGB,
|
|
35
|
+
RESET,
|
|
36
|
+
normalize_colors,
|
|
37
|
+
preview_ansi,
|
|
38
|
+
register_color_dialect,
|
|
39
|
+
registered_dialects,
|
|
40
|
+
unregister_color_dialect,
|
|
41
|
+
upgrade_to_256,
|
|
42
|
+
upgrade_to_truecolor,
|
|
43
|
+
)
|
|
44
|
+
from provide.uterm.auth import (
|
|
45
|
+
AuthorizedKeysFileResolver,
|
|
46
|
+
NullResolver,
|
|
47
|
+
ResolvedIdentity,
|
|
48
|
+
SSHKeyResolver,
|
|
49
|
+
fingerprint_from_openssh_blob,
|
|
50
|
+
)
|
|
51
|
+
from provide.uterm.colors import (
|
|
52
|
+
apply_color_mode,
|
|
53
|
+
downgrade_to_16,
|
|
54
|
+
downgrade_to_256,
|
|
55
|
+
rgb_to_16_index,
|
|
56
|
+
rgb_to_256,
|
|
57
|
+
)
|
|
58
|
+
from provide.uterm.control_channel_builders import (
|
|
59
|
+
make_identity,
|
|
60
|
+
make_link_patterns,
|
|
61
|
+
make_presence_update,
|
|
62
|
+
make_resume,
|
|
63
|
+
make_resume_failed,
|
|
64
|
+
make_resume_ok,
|
|
65
|
+
make_session_token,
|
|
66
|
+
)
|
|
67
|
+
from provide.uterm.control_channel_patterns import LinkPattern, LinkPatternRegistry
|
|
68
|
+
from provide.uterm.file_io import load_ans, load_palette, load_txt
|
|
69
|
+
from provide.uterm.line_editor import LineEditor
|
|
70
|
+
from provide.uterm.render.segments import (
|
|
71
|
+
SEGMENT_COLOR_NAMES,
|
|
72
|
+
Segment,
|
|
73
|
+
ansi_to_segments,
|
|
74
|
+
tokens_to_segments,
|
|
75
|
+
)
|
|
76
|
+
from provide.uterm.screen import (
|
|
77
|
+
clean_screen_for_display,
|
|
78
|
+
decode_cp437,
|
|
79
|
+
encode_cp437,
|
|
80
|
+
extract_action_tags,
|
|
81
|
+
extract_key_value_pairs,
|
|
82
|
+
extract_menu_options,
|
|
83
|
+
extract_numbered_list,
|
|
84
|
+
normalize_terminal_text,
|
|
85
|
+
strip_ansi,
|
|
86
|
+
)
|
|
87
|
+
from provide.uterm.ws_bytes import (
|
|
88
|
+
channel_str_to_bytes,
|
|
89
|
+
ws_frame_to_channel_str,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
__all__ = [
|
|
93
|
+
"__version__",
|
|
94
|
+
# ansi
|
|
95
|
+
"CLEAR_SCREEN",
|
|
96
|
+
"BOLD",
|
|
97
|
+
"RESET",
|
|
98
|
+
"DEFAULT_PALETTE",
|
|
99
|
+
"DEFAULT_RGB",
|
|
100
|
+
"normalize_colors",
|
|
101
|
+
"SEGMENT_COLOR_NAMES",
|
|
102
|
+
"Segment",
|
|
103
|
+
"ansi_to_segments",
|
|
104
|
+
"tokens_to_segments",
|
|
105
|
+
"preview_ansi",
|
|
106
|
+
"register_color_dialect",
|
|
107
|
+
"unregister_color_dialect",
|
|
108
|
+
"registered_dialects",
|
|
109
|
+
"upgrade_to_256",
|
|
110
|
+
"upgrade_to_truecolor",
|
|
111
|
+
# colors — downgrade counterparts
|
|
112
|
+
"apply_color_mode",
|
|
113
|
+
"downgrade_to_16",
|
|
114
|
+
"downgrade_to_256",
|
|
115
|
+
"rgb_to_16_index",
|
|
116
|
+
"rgb_to_256",
|
|
117
|
+
# file_io
|
|
118
|
+
"load_ans",
|
|
119
|
+
"load_txt",
|
|
120
|
+
"load_palette",
|
|
121
|
+
# line_editor
|
|
122
|
+
"LineEditor",
|
|
123
|
+
# screen
|
|
124
|
+
"strip_ansi",
|
|
125
|
+
"normalize_terminal_text",
|
|
126
|
+
"decode_cp437",
|
|
127
|
+
"encode_cp437",
|
|
128
|
+
"extract_action_tags",
|
|
129
|
+
"clean_screen_for_display",
|
|
130
|
+
"extract_menu_options",
|
|
131
|
+
"extract_numbered_list",
|
|
132
|
+
"extract_key_value_pairs",
|
|
133
|
+
# ws_bytes — lossless byte ↔ str shim for the inline control channel
|
|
134
|
+
"channel_str_to_bytes",
|
|
135
|
+
"ws_frame_to_channel_str",
|
|
136
|
+
# control_channel_builders — typed builders for ControlChannel protocol messages
|
|
137
|
+
"make_identity",
|
|
138
|
+
"make_session_token",
|
|
139
|
+
"make_resume",
|
|
140
|
+
"make_resume_ok",
|
|
141
|
+
"make_resume_failed",
|
|
142
|
+
"make_link_patterns",
|
|
143
|
+
"make_presence_update",
|
|
144
|
+
# control_channel_patterns — server-side link_patterns registry
|
|
145
|
+
"LinkPattern",
|
|
146
|
+
"LinkPatternRegistry",
|
|
147
|
+
# auth — pluggable SSH pubkey → identity
|
|
148
|
+
"AuthorizedKeysFileResolver",
|
|
149
|
+
"NullResolver",
|
|
150
|
+
"ResolvedIdentity",
|
|
151
|
+
"SSHKeyResolver",
|
|
152
|
+
# terminal frame lifecycle
|
|
153
|
+
"TerminalFrame",
|
|
154
|
+
"TerminalFrameDisconnectedError",
|
|
155
|
+
"fingerprint_from_openssh_blob",
|
|
156
|
+
# control-plane namespaces
|
|
157
|
+
"control_channel_namespace",
|
|
158
|
+
"control_plane_namespace",
|
|
159
|
+
"frames",
|
|
160
|
+
"channels",
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
from provide.uterm.control import channel as control_channel_namespace
|
|
164
|
+
from provide.uterm.control import plane as control_plane_namespace
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def __getattr__(name: str) -> object:
|
|
168
|
+
if name == "frames":
|
|
169
|
+
return import_module("provide.uterm.frames")
|
|
170
|
+
if name == "channels":
|
|
171
|
+
return import_module("provide.uterm.channels")
|
|
172
|
+
raise AttributeError(name)
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
"""Built-in BBS color-token dialect handlers for :mod:`provide.uterm.ansi`.
|
|
6
|
+
|
|
7
|
+
Self-contained ``str -> str`` converters for the extended-token, tilde-code,
|
|
8
|
+
TWGS brace-token, and pipe-code dialects, plus their lookup tables. Split out
|
|
9
|
+
of ``ansi.py`` to keep each module within the per-file line budget; the public
|
|
10
|
+
surface (``normalize_colors`` + the dialect registry) stays in ``ansi.py``,
|
|
11
|
+
which registers these handlers at import time.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
|
|
18
|
+
_EXT_TOKEN_RE = re.compile(r"\{([FBPT])(\d{1,3})\}")
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# Preview helpers (private)
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
_PREVIEW_COLOR_MAP = {
|
|
25
|
+
"k": 30,
|
|
26
|
+
"r": 31,
|
|
27
|
+
"g": 32,
|
|
28
|
+
"y": 33,
|
|
29
|
+
"b": 34,
|
|
30
|
+
"m": 35,
|
|
31
|
+
"c": 36,
|
|
32
|
+
"w": 37,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_TILDE_MAP: dict[str, tuple[str, str]] = {
|
|
36
|
+
"1": ("+", "g"),
|
|
37
|
+
"2": ("+", "w"),
|
|
38
|
+
"3": ("+", "c"),
|
|
39
|
+
"4": ("+", "r"),
|
|
40
|
+
"5": ("+", "m"),
|
|
41
|
+
"6": ("+", "y"),
|
|
42
|
+
"7": ("-", "w"),
|
|
43
|
+
"0": ("-", "x"),
|
|
44
|
+
"r": ("+", "r"),
|
|
45
|
+
"R": ("+", "r"),
|
|
46
|
+
"g": ("+", "g"),
|
|
47
|
+
"G": ("+", "g"),
|
|
48
|
+
"y": ("+", "y"),
|
|
49
|
+
"Y": ("+", "y"),
|
|
50
|
+
"b": ("+", "b"),
|
|
51
|
+
"B": ("+", "b"),
|
|
52
|
+
"m": ("+", "m"),
|
|
53
|
+
"M": ("+", "m"),
|
|
54
|
+
"c": ("+", "c"),
|
|
55
|
+
"C": ("+", "c"),
|
|
56
|
+
"w": ("+", "w"),
|
|
57
|
+
"W": ("+", "w"),
|
|
58
|
+
"d": ("-", "w"),
|
|
59
|
+
"D": ("-", "w"),
|
|
60
|
+
"E": ("+", "r"),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_BRACE_TOKEN_MAP: dict[str, str] = {
|
|
64
|
+
"{+c}": "\x1b[1;36m",
|
|
65
|
+
"{-c}": "\x1b[0;36m",
|
|
66
|
+
"{+r}": "\x1b[1;31m",
|
|
67
|
+
"{-r}": "\x1b[0;31m",
|
|
68
|
+
"{+g}": "\x1b[1;32m",
|
|
69
|
+
"{-g}": "\x1b[0;32m",
|
|
70
|
+
"{+y}": "\x1b[1;33m",
|
|
71
|
+
"{-y}": "\x1b[0;33m",
|
|
72
|
+
"{+b}": "\x1b[1;34m",
|
|
73
|
+
"{-b}": "\x1b[0;34m",
|
|
74
|
+
"{+m}": "\x1b[1;35m",
|
|
75
|
+
"{-m}": "\x1b[0;35m",
|
|
76
|
+
"{+w}": "\x1b[1;37m",
|
|
77
|
+
"{+Bw}": "\x1b[1;37m",
|
|
78
|
+
"{-w}": "\x1b[0;37m",
|
|
79
|
+
"{+k}": "\x1b[1;30m",
|
|
80
|
+
"{-k}": "\x1b[0;30m",
|
|
81
|
+
"{-x}": "\x1b[0m",
|
|
82
|
+
"{NK}": "\x1b[0m",
|
|
83
|
+
"{T}": "\x1b[1m",
|
|
84
|
+
"{t}": "\x1b[0m",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _emit_color(polarity: str, color_char: str) -> str:
|
|
89
|
+
if color_char == "x":
|
|
90
|
+
return "\x1b[0m"
|
|
91
|
+
code = _PREVIEW_COLOR_MAP.get(color_char)
|
|
92
|
+
if code is None:
|
|
93
|
+
return ""
|
|
94
|
+
if polarity == "+":
|
|
95
|
+
return f"\x1b[0;1;{code}m"
|
|
96
|
+
return f"\x1b[0;{code}m"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Pre-build extended token lookups to eliminate per-match f-string creation.
|
|
100
|
+
_EXT_F_LOOKUP = tuple(f"\x1b[38;5;{i}m" for i in range(256))
|
|
101
|
+
_EXT_B_LOOKUP = tuple(f"\x1b[48;5;{i}m" for i in range(256))
|
|
102
|
+
_EXT_P_LOOKUP = tuple(f"\x1b[{(90 + (i % 8)) if (i % 16) >= 8 else (30 + (i % 8))}m" for i in range(16))
|
|
103
|
+
_EXT_T_LOOKUP = tuple(f"\x1b[{(100 + (i % 8)) if (i % 16) >= 8 else (40 + (i % 8))}m" for i in range(16))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _handle_extended_tokens(text: str) -> str:
|
|
107
|
+
"""Convert {F###}/{B###}/{P#}/{T#} extended color tokens to ANSI escapes."""
|
|
108
|
+
parts: list[str] = []
|
|
109
|
+
last_end = 0
|
|
110
|
+
for m in _EXT_TOKEN_RE.finditer(text):
|
|
111
|
+
parts.append(text[last_end : m.start()])
|
|
112
|
+
kind = m.group(1)
|
|
113
|
+
val = int(m.group(2))
|
|
114
|
+
if kind == "F":
|
|
115
|
+
parts.append(_EXT_F_LOOKUP[val] if val < 256 else f"\x1b[38;5;{val}m")
|
|
116
|
+
elif kind == "B":
|
|
117
|
+
parts.append(_EXT_B_LOOKUP[val] if val < 256 else f"\x1b[48;5;{val}m")
|
|
118
|
+
elif kind == "P":
|
|
119
|
+
parts.append(_EXT_P_LOOKUP[val % 16])
|
|
120
|
+
elif kind == "T": # pragma: no cover — truecolor reserved for future use
|
|
121
|
+
parts.append(_EXT_T_LOOKUP[val % 16])
|
|
122
|
+
last_end = m.end()
|
|
123
|
+
if last_end == 0:
|
|
124
|
+
return text # no matches
|
|
125
|
+
parts.append(text[last_end:])
|
|
126
|
+
return "".join(parts)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
_TILDE_RE = re.compile(r"~(.)")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# Pre-build tilde code → ANSI escape lookup to avoid per-match _emit_color calls.
|
|
133
|
+
_TILDE_LOOKUP: dict[str, str] = {}
|
|
134
|
+
for _tc, (_pol, _cc) in _TILDE_MAP.items():
|
|
135
|
+
_seq = _emit_color(_pol, _cc)
|
|
136
|
+
if _seq: # pragma: no cover — _seq always truthy for all defined tilde codes
|
|
137
|
+
_TILDE_LOOKUP[_tc] = _seq
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _handle_tilde_codes(text: str) -> str:
|
|
141
|
+
parts = _TILDE_RE.split(text)
|
|
142
|
+
if len(parts) == 1:
|
|
143
|
+
return text
|
|
144
|
+
for i in range(1, len(parts), 2):
|
|
145
|
+
parts[i] = _TILDE_LOOKUP.get(parts[i], f"~{parts[i]}")
|
|
146
|
+
return "".join(parts)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
_BRACE_3_RE = re.compile(r"(\{[+\-][a-zA-Z]\}|\{NK\}|\{T\}|\{t\})")
|
|
150
|
+
_BRACE_4_RE = re.compile(r"(\{[+\-]Bw\})")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _handle_brace_tokens(text: str) -> str:
|
|
154
|
+
"""Convert ``{+c}``/``{-x}`` brace tokens to ANSI escapes.
|
|
155
|
+
|
|
156
|
+
Includes the TWGS-specific ``{+Bw}`` header token in addition to the
|
|
157
|
+
single-character color tags.
|
|
158
|
+
"""
|
|
159
|
+
# Handle 5-char tokens first (longest match first to avoid conflicts)
|
|
160
|
+
parts4 = _BRACE_4_RE.split(text)
|
|
161
|
+
if len(parts4) > 1:
|
|
162
|
+
for i in range(1, len(parts4), 2):
|
|
163
|
+
parts4[i] = _BRACE_TOKEN_MAP.get(parts4[i], parts4[i])
|
|
164
|
+
text = "".join(parts4)
|
|
165
|
+
# Then 3-char and other tokens
|
|
166
|
+
parts3 = _BRACE_3_RE.split(text)
|
|
167
|
+
if len(parts3) > 1:
|
|
168
|
+
for i in range(1, len(parts3), 2):
|
|
169
|
+
parts3[i] = _BRACE_TOKEN_MAP.get(parts3[i], parts3[i])
|
|
170
|
+
return "".join(parts3)
|
|
171
|
+
return text
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
# Pipe codes (|00-|23) — most common BBS color format
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
_PIPE_RE = re.compile(r"\|(\d{2})")
|
|
179
|
+
|
|
180
|
+
# DOS color order → ANSI SGR codes
|
|
181
|
+
_DOS_TO_ANSI_FG = [30, 34, 32, 36, 31, 35, 33, 37] # |00-|07 (dim)
|
|
182
|
+
_DOS_TO_ANSI_BG = [40, 44, 42, 46, 41, 45, 43, 47] # |16-|23
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
_PIPE_LOOKUP: dict[str, str] = {}
|
|
186
|
+
for _i in range(24):
|
|
187
|
+
_key = f"{_i:02d}"
|
|
188
|
+
if _i <= 7:
|
|
189
|
+
_PIPE_LOOKUP[_key] = f"\x1b[{_DOS_TO_ANSI_FG[_i]}m"
|
|
190
|
+
elif _i <= 15:
|
|
191
|
+
_PIPE_LOOKUP[_key] = f"\x1b[{_DOS_TO_ANSI_FG[_i - 8] + 60}m"
|
|
192
|
+
else:
|
|
193
|
+
_PIPE_LOOKUP[_key] = f"\x1b[{_DOS_TO_ANSI_BG[_i - 16]}m"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _handle_pipe_codes(text: str) -> str:
|
|
197
|
+
# split() with a capturing group keeps the matched group in the result list,
|
|
198
|
+
# alternating: [text, match, text, match, ...]. We replace matches in-place
|
|
199
|
+
# via the lookup, avoiding re.sub's per-match string rebuild.
|
|
200
|
+
parts = _PIPE_RE.split(text)
|
|
201
|
+
if len(parts) == 1:
|
|
202
|
+
return text # no matches — return original without allocation
|
|
203
|
+
for i in range(1, len(parts), 2):
|
|
204
|
+
parts[i] = _PIPE_LOOKUP.get(parts[i], f"|{parts[i]}")
|
|
205
|
+
return "".join(parts)
|