evp 0.0.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.
Potentially problematic release.
This version of evp might be problematic. Click here for more details.
- evp-0.0.1/.cargo/config.toml +7 -0
- evp-0.0.1/.github/dependabot.yml +8 -0
- evp-0.0.1/.github/workflows/build.yml +109 -0
- evp-0.0.1/.github/workflows/ci.yml +55 -0
- evp-0.0.1/.github/workflows/docs.yml +56 -0
- evp-0.0.1/.github/workflows/release.yml +37 -0
- evp-0.0.1/.gitignore +33 -0
- evp-0.0.1/.pre-commit-config.yaml +31 -0
- evp-0.0.1/CLAUDE.md +148 -0
- evp-0.0.1/Cargo.lock +204 -0
- evp-0.0.1/Cargo.toml +27 -0
- evp-0.0.1/LICENSE +21 -0
- evp-0.0.1/PKG-INFO +62 -0
- evp-0.0.1/PLAN.md +468 -0
- evp-0.0.1/README.md +40 -0
- evp-0.0.1/benchmarks/README.md +61 -0
- evp-0.0.1/benchmarks/ab.py +94 -0
- evp-0.0.1/benchmarks/suite.py +278 -0
- evp-0.0.1/benchmarks/tips.py +206 -0
- evp-0.0.1/benchmarks/workloads.py +112 -0
- evp-0.0.1/docs/api.md +73 -0
- evp-0.0.1/docs/changelog.md +20 -0
- evp-0.0.1/docs/extra.css +83 -0
- evp-0.0.1/docs/index.md +76 -0
- evp-0.0.1/docs/maintaining.md +61 -0
- evp-0.0.1/docs/performance.md +56 -0
- evp-0.0.1/justfile +143 -0
- evp-0.0.1/mise.toml +15 -0
- evp-0.0.1/mkdocs.yml +49 -0
- evp-0.0.1/plans/upgrade-libghostty-vt.md +104 -0
- evp-0.0.1/pyproject.toml +126 -0
- evp-0.0.1/python/evp/__init__.py +67 -0
- evp-0.0.1/python/evp/_evp.pyi +750 -0
- evp-0.0.1/python/evp/py.typed +0 -0
- evp-0.0.1/spike/bench.py +167 -0
- evp-0.0.1/spike/counterweight-tests.sh +23 -0
- evp-0.0.1/spike/emulator.py +270 -0
- evp-0.0.1/src/encode.rs +151 -0
- evp-0.0.1/src/enums.rs +167 -0
- evp-0.0.1/src/errors.rs +20 -0
- evp-0.0.1/src/format.rs +128 -0
- evp-0.0.1/src/input.rs +231 -0
- evp-0.0.1/src/lib.rs +32 -0
- evp-0.0.1/src/render.rs +708 -0
- evp-0.0.1/src/style.rs +135 -0
- evp-0.0.1/src/terminal.rs +443 -0
- evp-0.0.1/tests/strategies.py +113 -0
- evp-0.0.1/tests/test_encode.py +85 -0
- evp-0.0.1/tests/test_encode_properties.py +284 -0
- evp-0.0.1/tests/test_format.py +59 -0
- evp-0.0.1/tests/test_input.py +159 -0
- evp-0.0.1/tests/test_render_state.py +265 -0
- evp-0.0.1/tests/test_render_state_properties.py +218 -0
- evp-0.0.1/tests/test_stub.py +62 -0
- evp-0.0.1/tests/test_terminal.py +241 -0
- evp-0.0.1/tests/test_threads.py +97 -0
- evp-0.0.1/uv.lock +1159 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
[build]
|
|
2
|
+
# pyo3's reference pool defers decrefs of `Py<T>` values dropped while detached from the
|
|
3
|
+
# interpreter, and once it exists every call into evp locks its global mutex. evp never detaches,
|
|
4
|
+
# so it never needs the pool. Without it, a `Py<T>` dropped while detached aborts the process.
|
|
5
|
+
# A RUSTFLAGS environment variable replaces this list rather than extending it, so CI repeats the
|
|
6
|
+
# flag in its own rustflags.
|
|
7
|
+
rustflags = ["--cfg", "pyo3_disable_reference_pool"]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
name: Build
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
workflow_call:
|
|
9
|
+
|
|
10
|
+
permissions: {}
|
|
11
|
+
|
|
12
|
+
env:
|
|
13
|
+
CARGO_TERM_COLOR: always
|
|
14
|
+
|
|
15
|
+
# The wheels and sdist release.yml publishes, built and tested here on every push too,
|
|
16
|
+
# so a release runs the build a pull request already ran.
|
|
17
|
+
# No caches, unlike ci.yml: each build clones Ghostty afresh,
|
|
18
|
+
# so nothing another run wrote can end up in a published wheel.
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
wheels:
|
|
22
|
+
name: Wheel (${{ matrix.os }})
|
|
23
|
+
|
|
24
|
+
strategy:
|
|
25
|
+
fail-fast: false
|
|
26
|
+
matrix:
|
|
27
|
+
# macos-15, not macos-latest: on macOS 26, Zig 0.15.2 (Ghostty's, for libghostty-vt 0.2.2) can't link
|
|
28
|
+
# its own build runner against libSystem. A Ghostty that takes a newer Zig can move to macos-latest.
|
|
29
|
+
os: [ubuntu-latest, ubuntu-24.04-arm, macos-15, macos-15-intel, windows-latest]
|
|
30
|
+
|
|
31
|
+
runs-on: ${{ matrix.os }}
|
|
32
|
+
timeout-minutes: 60
|
|
33
|
+
|
|
34
|
+
permissions:
|
|
35
|
+
contents: read
|
|
36
|
+
|
|
37
|
+
defaults:
|
|
38
|
+
run:
|
|
39
|
+
shell: bash
|
|
40
|
+
|
|
41
|
+
steps:
|
|
42
|
+
- name: Check out repository
|
|
43
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
44
|
+
with:
|
|
45
|
+
persist-credentials: false
|
|
46
|
+
|
|
47
|
+
# Zig and Rust for the macOS and Windows builds, which run on the runner, and uv for testing the wheel.
|
|
48
|
+
- name: Install tools
|
|
49
|
+
uses: jdx/mise-action@2d8d4cafcbd33be2ea37d2b6f5ad595363d1f1ca # v5.1.1
|
|
50
|
+
with:
|
|
51
|
+
version: 2026.10.12
|
|
52
|
+
cache: false
|
|
53
|
+
|
|
54
|
+
# On Linux this builds in maturin-action's manylinux2014 container, which mounts only the workspace,
|
|
55
|
+
# so the container installs its own mise. The musl build of mise is the one that runs on manylinux2014's glibc 2.17.
|
|
56
|
+
# Keep MISE_VERSION in step with mise-action's `version` above, and maturin-version with uv.lock's maturin.
|
|
57
|
+
- name: Build wheel
|
|
58
|
+
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
|
|
59
|
+
with:
|
|
60
|
+
maturin-version: v1.15.0
|
|
61
|
+
manylinux: auto
|
|
62
|
+
args: --release --out dist
|
|
63
|
+
before-script-linux: |
|
|
64
|
+
curl -fsSL https://mise.run | MISE_VERSION=v2026.10.12 MISE_INSTALL_MUSL=1 sh
|
|
65
|
+
export PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:$PATH"
|
|
66
|
+
mise trust
|
|
67
|
+
mise install
|
|
68
|
+
|
|
69
|
+
# One abi3 wheel serves every supported Python, so test it against each of ci.yml's.
|
|
70
|
+
- name: Test wheel
|
|
71
|
+
run: |
|
|
72
|
+
for python in 3.13 3.14 3.15; do
|
|
73
|
+
uv sync --locked --no-install-project --python "$python"
|
|
74
|
+
uv pip install dist/*.whl
|
|
75
|
+
uv run --no-sync pytest
|
|
76
|
+
done
|
|
77
|
+
|
|
78
|
+
- name: Upload wheel
|
|
79
|
+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
80
|
+
with:
|
|
81
|
+
name: wheels-${{ matrix.os }}
|
|
82
|
+
path: dist
|
|
83
|
+
|
|
84
|
+
sdist:
|
|
85
|
+
name: Sdist
|
|
86
|
+
runs-on: ubuntu-latest
|
|
87
|
+
timeout-minutes: 15
|
|
88
|
+
|
|
89
|
+
permissions:
|
|
90
|
+
contents: read
|
|
91
|
+
|
|
92
|
+
steps:
|
|
93
|
+
- name: Check out repository
|
|
94
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
95
|
+
with:
|
|
96
|
+
persist-credentials: false
|
|
97
|
+
|
|
98
|
+
- name: Build sdist
|
|
99
|
+
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
|
|
100
|
+
with:
|
|
101
|
+
maturin-version: v1.15.0
|
|
102
|
+
command: sdist
|
|
103
|
+
args: --out dist
|
|
104
|
+
|
|
105
|
+
- name: Upload sdist
|
|
106
|
+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
107
|
+
with:
|
|
108
|
+
name: sdist
|
|
109
|
+
path: dist
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
env:
|
|
13
|
+
CARGO_TERM_COLOR: always
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
ci:
|
|
17
|
+
strategy:
|
|
18
|
+
fail-fast: false
|
|
19
|
+
matrix:
|
|
20
|
+
# macos-15, not macos-latest: on macOS 26, Zig 0.15.2 (Ghostty's, for libghostty-vt 0.2.2) can't link
|
|
21
|
+
# its own build runner against libSystem. A Ghostty that takes a newer Zig can move to macos-latest.
|
|
22
|
+
os: [ubuntu-latest, ubuntu-24.04-arm, macos-15, macos-15-intel, windows-latest]
|
|
23
|
+
python-version: ["3.13", "3.14", "3.15"]
|
|
24
|
+
|
|
25
|
+
runs-on: ${{ matrix.os }}
|
|
26
|
+
|
|
27
|
+
env:
|
|
28
|
+
UV_PYTHON: ${{ matrix.python-version }}
|
|
29
|
+
# Setting RUSTFLAGS replaces .cargo/config.toml's rustflags, so repeat them here.
|
|
30
|
+
RUSTFLAGS: -D warnings --cfg pyo3_disable_reference_pool
|
|
31
|
+
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
34
|
+
with:
|
|
35
|
+
persist-credentials: false
|
|
36
|
+
|
|
37
|
+
- uses: jdx/mise-action@2d8d4cafcbd33be2ea37d2b6f5ad595363d1f1ca # v5.1.1
|
|
38
|
+
with:
|
|
39
|
+
version: 2026.10.12
|
|
40
|
+
|
|
41
|
+
# Without the cache every job clones and builds Ghostty, which libghostty-vt-sys's
|
|
42
|
+
# build.rs does into target/.
|
|
43
|
+
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
44
|
+
with:
|
|
45
|
+
path: |
|
|
46
|
+
~/.cargo/registry
|
|
47
|
+
~/.cargo/git
|
|
48
|
+
target
|
|
49
|
+
key: cargo-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-${{ hashFiles('Cargo.lock', 'mise.toml') }}
|
|
50
|
+
|
|
51
|
+
- name: Fix
|
|
52
|
+
run: just pre-commit-args="--verbose --all-files --show-diff-on-failure" fix
|
|
53
|
+
|
|
54
|
+
- name: Test
|
|
55
|
+
run: just test
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
name: Docs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions: {}
|
|
9
|
+
|
|
10
|
+
concurrency:
|
|
11
|
+
group: pages
|
|
12
|
+
cancel-in-progress: false
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
build:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
permissions:
|
|
18
|
+
contents: read
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
21
|
+
with:
|
|
22
|
+
persist-credentials: false
|
|
23
|
+
|
|
24
|
+
- uses: jdx/mise-action@2d8d4cafcbd33be2ea37d2b6f5ad595363d1f1ca # v5.1.1
|
|
25
|
+
with:
|
|
26
|
+
version: 2026.10.12
|
|
27
|
+
|
|
28
|
+
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
29
|
+
with:
|
|
30
|
+
path: |
|
|
31
|
+
~/.cargo/registry
|
|
32
|
+
~/.cargo/git
|
|
33
|
+
target
|
|
34
|
+
key: cargo-docs-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Cargo.lock', 'mise.toml') }}
|
|
35
|
+
|
|
36
|
+
- name: Build docs
|
|
37
|
+
run: just docs-build
|
|
38
|
+
|
|
39
|
+
- name: Upload pages artifact
|
|
40
|
+
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
|
41
|
+
with:
|
|
42
|
+
path: site/
|
|
43
|
+
|
|
44
|
+
deploy:
|
|
45
|
+
needs: build
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
permissions:
|
|
48
|
+
pages: write
|
|
49
|
+
id-token: write
|
|
50
|
+
environment:
|
|
51
|
+
name: github-pages
|
|
52
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
53
|
+
steps:
|
|
54
|
+
- name: Deploy to GitHub Pages
|
|
55
|
+
id: deployment
|
|
56
|
+
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions: {}
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
name: Build
|
|
12
|
+
uses: $/.github/workflows/build.yml
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
publish:
|
|
17
|
+
name: Publish to PyPI
|
|
18
|
+
needs: [build]
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
timeout-minutes: 15
|
|
21
|
+
|
|
22
|
+
environment:
|
|
23
|
+
name: pypi
|
|
24
|
+
url: https://pypi.org/p/evp
|
|
25
|
+
|
|
26
|
+
permissions:
|
|
27
|
+
id-token: write # Trusted publishing: PyPI trusts this workflow and environment, so no API token exists.
|
|
28
|
+
|
|
29
|
+
steps:
|
|
30
|
+
- name: Download wheels and sdist
|
|
31
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
32
|
+
with:
|
|
33
|
+
path: dist
|
|
34
|
+
merge-multiple: true
|
|
35
|
+
|
|
36
|
+
- name: Publish to PyPI
|
|
37
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
evp-0.0.1/.gitignore
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Rust
|
|
2
|
+
target/
|
|
3
|
+
**/*.rs.bk
|
|
4
|
+
*.pdb
|
|
5
|
+
**/mutants.out*/
|
|
6
|
+
rustc-ice-*.txt
|
|
7
|
+
|
|
8
|
+
# Zig (libghostty-vt-sys builds Ghostty under target/, but a standalone Ghostty build writes these)
|
|
9
|
+
.zig-cache/
|
|
10
|
+
zig-cache/
|
|
11
|
+
zig-out/
|
|
12
|
+
zig-pkg/
|
|
13
|
+
|
|
14
|
+
# Python
|
|
15
|
+
__pycache__/
|
|
16
|
+
*.py[cod]
|
|
17
|
+
.venv/
|
|
18
|
+
python/evp/*.so
|
|
19
|
+
python/evp/*.pyd
|
|
20
|
+
build/
|
|
21
|
+
dist/
|
|
22
|
+
*.egg-info/
|
|
23
|
+
.mypy_cache/
|
|
24
|
+
.pytest_cache/
|
|
25
|
+
.ruff_cache/
|
|
26
|
+
.hypothesis/
|
|
27
|
+
.coverage
|
|
28
|
+
.coverage.*
|
|
29
|
+
htmlcov/
|
|
30
|
+
mutants/
|
|
31
|
+
|
|
32
|
+
# Docs
|
|
33
|
+
site/
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
|
+
rev: v6.0.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: trailing-whitespace
|
|
6
|
+
- id: end-of-file-fixer
|
|
7
|
+
- id: check-yaml
|
|
8
|
+
- id: check-toml
|
|
9
|
+
- id: check-merge-conflict
|
|
10
|
+
- id: check-added-large-files
|
|
11
|
+
|
|
12
|
+
- repo: https://github.com/python-jsonschema/check-jsonschema
|
|
13
|
+
rev: 0.38.2
|
|
14
|
+
hooks:
|
|
15
|
+
- id: check-github-workflows
|
|
16
|
+
- id: check-dependabot
|
|
17
|
+
|
|
18
|
+
- repo: local
|
|
19
|
+
hooks:
|
|
20
|
+
- id: just-fmt
|
|
21
|
+
name: Format justfile
|
|
22
|
+
entry: just --fmt --unstable
|
|
23
|
+
language: system
|
|
24
|
+
files: justfile
|
|
25
|
+
pass_filenames: false
|
|
26
|
+
|
|
27
|
+
- id: fix
|
|
28
|
+
name: just fix
|
|
29
|
+
entry: just _fix
|
|
30
|
+
language: system
|
|
31
|
+
pass_filenames: false
|
evp-0.0.1/CLAUDE.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
# evp
|
|
6
|
+
|
|
7
|
+
A Python binding to libghostty-vt, Ghostty's terminal emulator library, built with PyO3/maturin on
|
|
8
|
+
[libghostty-rs](https://github.com/Uzaaft/libghostty-rs)'s `libghostty-vt` crate.
|
|
9
|
+
It replaces pyghostty as [counterweight](https://github.com/JoshKarpel/counterweight)'s terminal emulator binding,
|
|
10
|
+
as [waxy](https://github.com/JoshKarpel/waxy) is counterweight's binding to taffy; waxy is the model for this repo's layout, tooling and habits.
|
|
11
|
+
|
|
12
|
+
## Key Files
|
|
13
|
+
|
|
14
|
+
- `PLAN.md`: the design (why own the binding, the API's shape, language, build and release, the work in steps)
|
|
15
|
+
and what the spike measured. `plans/upgrade-libghostty-vt.md`: what moving past libghostty-vt 0.2.2 gains and changes.
|
|
16
|
+
- `python/evp/_evp.pyi`: the type stub, and the source of the API reference: mkdocstrings renders `docs/api.md` from its docstrings.
|
|
17
|
+
- `spike/`: `emulator.py` is counterweight's `Emulator` rebuilt on evp; `bench.py` times it against counterweight's,
|
|
18
|
+
and `counterweight-tests.sh` runs counterweight's `tests/test_emulator.py` against it.
|
|
19
|
+
Both need a Python with evp and counterweight (from a checkout) installed; their docstrings and header say how to run them.
|
|
20
|
+
- `docs/maintaining.md`: building, releasing, upgrading libghostty-vt (and Zig with it), adding a Python version.
|
|
21
|
+
- `benchmarks/`: the timing suite and build-against-build comparison for hot-path changes;
|
|
22
|
+
its `README.md` covers the workflow (release builds, comparing builds rather than runs, profiling).
|
|
23
|
+
`tips.py` measures the ratios `docs/performance.md` states; rerun it when a change could move them.
|
|
24
|
+
- `tests/test_*_properties.py`: Hypothesis property tests, on the VT output strategies in `tests/strategies.py`.
|
|
25
|
+
The render state's oracle is a twin terminal fed the same calls and read by a fresh `RenderState` each time,
|
|
26
|
+
so whatever `frame` reuses must read as a from-scratch read does.
|
|
27
|
+
|
|
28
|
+
## Architecture
|
|
29
|
+
|
|
30
|
+
The Rust source (`src/`) exposes a flat PyO3 module `_evp`, which `python/evp/__init__.py` re-exports.
|
|
31
|
+
|
|
32
|
+
| File | Contents |
|
|
33
|
+
|------|----------|
|
|
34
|
+
| `src/lib.rs` | PyO3 module definition, wires each file's `register()`; `Owned`, the `Send` wrapper for libghostty objects |
|
|
35
|
+
| `src/errors.rs` | `GhosttyError`, and `ghostty_error` for `map_err` on every libghostty-vt `Result` |
|
|
36
|
+
| `src/enums.rs` | The Python `IntEnum`s and `IntFlag`s (`Key`, `Mods`, `MouseButton`, ...), built at import from libghostty's C constants |
|
|
37
|
+
| `src/style.rs` | `Style`, a hashable value copy of libghostty's style, with colors as `None`, a palette index or RGB |
|
|
38
|
+
| `src/terminal.rs` | `Terminal`: feeding bytes, resizing, modes, title, colors, viewport, its callbacks; `DeviceAttributes` |
|
|
39
|
+
| `src/render.rs` | `RenderState` and what it returns (`Frame`, `ScrolledOff`, `History`, `Row`) |
|
|
40
|
+
| `src/input.rs` | `KeyEncoder` and `MouseEncoder`, which read the terminal's modes for each event |
|
|
41
|
+
| `src/encode.rs` | The stateless encoders (focus, paste, color scheme, size report) and the width functions |
|
|
42
|
+
| `src/format.rs` | `Formatter` |
|
|
43
|
+
|
|
44
|
+
`RenderState` is where the work is. Three reads share one style table:
|
|
45
|
+
|
|
46
|
+
- **`frame()`** reads the screen. A row libghostty doesn't mark dirty is the last frame's `Row` at that position.
|
|
47
|
+
A dirty row's raw cells are compared with every row of the last frame (`find_unchanged`), so a row a scroll moved
|
|
48
|
+
comes back as the same `Row` object; consumers (counterweight's `Emulator`) reuse derived data by `Row` identity.
|
|
49
|
+
- **`scrolled_off()`** keeps an `Anchor`, a tracked grid reference on the row at the top of the screen at its last call.
|
|
50
|
+
The anchor follows that row into the scrollback, so its history position counts the rows that scrolled off since;
|
|
51
|
+
those rows are read through grid references (`HistoryCells`).
|
|
52
|
+
- **`history()`** reads the whole scrollback the same way.
|
|
53
|
+
|
|
54
|
+
`read_row` reads either source through the `Cells` trait (`RenderStateCells`, `HistoryCells`).
|
|
55
|
+
Styles are interned terminal-wide in `Styles`, and each of the three reads delivers the styles first used since any of them last did,
|
|
56
|
+
so rows name styles by index and a consumer converts each once. Index 0 is always the default style.
|
|
57
|
+
|
|
58
|
+
## Key Design Decisions
|
|
59
|
+
|
|
60
|
+
- **libghostty-vt 0.2.2, not a git pin of `master`.** 0.2.2's Ghostty has no row ids, no bulk raw cells (`cells_raw`),
|
|
61
|
+
no overscan and no scrollback limit in lines, in its C API as well as the Rust wrapper. evp works around each;
|
|
62
|
+
`plans/upgrade-libghostty-vt.md` lists the workarounds and what upgrading changes. Don't reach for `master`-only APIs.
|
|
63
|
+
- **The scrollback limit is bytes.** libghostty-rs documents 0.2.2's `Options::max_scrollback` as lines, but its Ghostty
|
|
64
|
+
treats it as bytes, frees a page (a few hundred KB) at a time, and keeps at least a page or two above zero.
|
|
65
|
+
So `Terminal` takes `max_scrollback_bytes`, fixed at construction.
|
|
66
|
+
- **Equal raw cells don't mean an equal row**, and `find_unchanged` and `read_row`'s `style_probes` guard both cases:
|
|
67
|
+
a page reuses a freed style id for another style, so each style id's style is read again from the dirty row;
|
|
68
|
+
and a cell's codepoints after its first live outside its raw value, so a row with a multi-codepoint grapheme cluster
|
|
69
|
+
(`style_probes: None`) is never reused by its cells. Tests in `tests/test_render_state.py` reproduce both.
|
|
70
|
+
- **No mirrored row or cell iterators.** A call from Python into Rust per cell is slower than `frame()` plus a Python loop
|
|
71
|
+
over every cell, so the bulk reads are the only way to read cells; PLAN.md's "Why no iterators?" has the measurements.
|
|
72
|
+
A cell a bulk read lacks (a hyperlink) belongs on `Row`, or in a point lookup for sparse reads, not in an iterator.
|
|
73
|
+
- **A terminal has at most one `RenderState`.** A second would take the first's dirty flags, so `RenderState::new`
|
|
74
|
+
refuses it through `Terminal.is_rendered`, an `Arc<AtomicBool>` its `Drop` clears.
|
|
75
|
+
- **No `#[pyclass(unsendable)]`**, as in waxy. libghostty's objects hold raw pointers, so `OwnedTerminal` and `Owned<T>`
|
|
76
|
+
carry `unsafe impl Send + Sync`, which rests on every access to them going through a `&mut self` method
|
|
77
|
+
(or `Py::borrow_mut`), so PyO3's borrow checking makes each access exclusive. A `&self` method that touches them breaks that.
|
|
78
|
+
- **Callback exceptions** are stored in `Terminal.raised` while libghostty runs the callback, then raised when the call
|
|
79
|
+
(`feed`, `resize`, `reset`) returns, first one wins. A callback runs while the terminal is borrowed,
|
|
80
|
+
so it's passed what it needs (the new title, the clipboard's contents) rather than calling back into the terminal.
|
|
81
|
+
- **Enums are Python `IntEnum`s and `IntFlag`s built at import** (`src/enums.rs`) from libghostty-vt-sys's constants,
|
|
82
|
+
so their values have one source. Rust takes them as plain integers and converts with `enums::parse`.
|
|
83
|
+
`tests/test_stub.py` checks the stub's members against them, and that the stub and module name the same things.
|
|
84
|
+
- **`MouseEncoder` syncs from the terminal only when its mouse modes or size changed**:
|
|
85
|
+
libghostty's `set_options_from_terminal` and `set_size` both forget the last cell, which breaks `track_last_cell`.
|
|
86
|
+
- **libghostty's docs and its behavior differ**, so probe before trusting the C header or libghostty-rs's docs.
|
|
87
|
+
Each of these is pinned by a test: device attributes get Ghostty's own reply with no callback, though the header says none;
|
|
88
|
+
`set_options_from_terminal` resets the mouse encoder's `track_last_cell` state, which no doc mentions;
|
|
89
|
+
`Options::max_scrollback` is bytes, not lines; the formatter's `trim` applies to plain text only;
|
|
90
|
+
a resize truncating the alternate screen can leave a wide character with no second column;
|
|
91
|
+
and the C0 controls 0x1c to 0x1f and DEL print into cells.
|
|
92
|
+
- **Ghostty's safety checks abort a debug build, and are undefined behavior in a release one.**
|
|
93
|
+
A value that overflows a conversion inside Ghostty (`@intFromFloat` on a mouse position) panics the whole process,
|
|
94
|
+
so evp bounds what it passes (`input.rs`'s `position`). The property tests are how these turn up:
|
|
95
|
+
a crash kills pytest rather than failing a test, so rerun with `-s -n0` to see Ghostty's panic.
|
|
96
|
+
One remains upstream: resizing with the viewport in the scrollback can fail the page list's integrity check;
|
|
97
|
+
`test_render_state_properties.py`'s `resize` rule works around it.
|
|
98
|
+
- **No ENQ or XTVERSION callbacks**: libghostty-rs's types tie the reply's `&str` to the terminal's lifetime,
|
|
99
|
+
so a Python callable can't supply one without leaking it. Ghostty sends nothing for ENQ and `libghostty` for XTVERSION.
|
|
100
|
+
- **No pyo3 reference pool**: `.cargo/config.toml` sets `--cfg pyo3_disable_reference_pool`, as waxy does, and CI's
|
|
101
|
+
`RUSTFLAGS` replaces that list, so `ci.yml` repeats the flag; keep the two in sync.
|
|
102
|
+
- **`abi3`**: maturin builds one limited-API wheel for CPython 3.13 and newer (`[tool.maturin] features`); the `abi3` Cargo feature is off for plain `cargo`.
|
|
103
|
+
- **Ghostty's Zig is pinned.** `libghostty-vt-sys`'s build script clones the Ghostty commit it pins and runs the `zig` on `PATH`,
|
|
104
|
+
and Ghostty refuses any Zig but the one its `build.zig.zon` names, so `zig` in `mise.toml` follows the libghostty-vt version in `Cargo.lock`.
|
|
105
|
+
|
|
106
|
+
## Commands
|
|
107
|
+
|
|
108
|
+
Every tool (Rust, Zig, just, uv) comes from `mise.toml`; Python dev tools come from uv's dev group. `just --list` shows the recipes.
|
|
109
|
+
|
|
110
|
+
- `just check` (alias `c`): fix, test, and a strict docs build. Run it before committing.
|
|
111
|
+
- `just test` (alias `t`): `maturin develop`, mypy, pytest, and `cargo test`.
|
|
112
|
+
- `just fix` (alias `f`): `git add --update`, then pre-commit (formatters, ruff, clippy, workflow checks).
|
|
113
|
+
- `just build`: `uv run maturin develop`, a debug build.
|
|
114
|
+
- `just bench`: a release build, then `benchmarks/suite.py`; `just bench-build` only builds.
|
|
115
|
+
- `just docs-build` / `just docs-serve`: the mkdocs site.
|
|
116
|
+
|
|
117
|
+
Override variables pass arguments through:
|
|
118
|
+
|
|
119
|
+
- `just pytest-args="-k scrolled_off" py-test`, or after `just build`, `uv run pytest tests/test_render_state.py::test_name`
|
|
120
|
+
- `just cargo-test-args="tracked_reference" rs-test`
|
|
121
|
+
- `just bench-args="-k frame --compare /tmp/before.json" bench`
|
|
122
|
+
- `just pre-commit-args="--all-files" fix`
|
|
123
|
+
|
|
124
|
+
Build and test gotchas:
|
|
125
|
+
|
|
126
|
+
- A clean build clones and compiles Ghostty (a few minutes), and needs network access.
|
|
127
|
+
- A debug build compiles Ghostty unoptimized too, so a test that feeds tens of thousands of lines takes a minute.
|
|
128
|
+
To fill the scrollback's pages quickly, use a wide terminal (1,000 columns) and fewer lines, as the pruning tests do.
|
|
129
|
+
- An old per-version extension (`python/evp/_evp.cpython-3XX-*.so`) left beside `_evp.abi3.so` shadows it,
|
|
130
|
+
so Python imports the stale build. Delete it.
|
|
131
|
+
- Benchmark only release builds (`just bench-build`); debug numbers say nothing about evp's speed.
|
|
132
|
+
- Editing `pyproject.toml` makes the next `uv run` rebuild evp through maturin's build backend, in release,
|
|
133
|
+
over whatever `just build` installed. `just test` rebuilds first; for anything else, `just build` again,
|
|
134
|
+
or run with `uv run --no-sync`.
|
|
135
|
+
|
|
136
|
+
## Dependencies
|
|
137
|
+
|
|
138
|
+
Add Python packages with `uv add` and Rust crates with `cargo add`, not by editing `pyproject.toml` or `Cargo.toml`,
|
|
139
|
+
so the lockfiles stay in sync. `pyproject.toml`'s `exclude-newer` and mise's `minimum_release_age` apply a 7-day cooldown.
|
|
140
|
+
|
|
141
|
+
## Changing the API
|
|
142
|
+
|
|
143
|
+
1. Add or change the Rust in the right `src/*.rs` file and register new classes in its `register()`.
|
|
144
|
+
2. Update `python/evp/_evp.pyi`, docstrings included, since they are the API reference, and `python/evp/__init__.py`'s exports.
|
|
145
|
+
3. Add the class to `docs/api.md` if it's new, and an entry to `docs/changelog.md`.
|
|
146
|
+
4. If counterweight would see the change, update `spike/emulator.py` and rerun `spike/counterweight-tests.sh`.
|
|
147
|
+
|
|
148
|
+
A new enum goes in `src/enums.rs`'s `ENUMS` and in the stub; `tests/test_stub.py` fails until the two agree.
|