py-screen-mux 20261101.6__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.
Files changed (110) hide show
  1. py_screen_mux-20261101.6/LICENSE +21 -0
  2. py_screen_mux-20261101.6/MANIFEST.in +3 -0
  3. py_screen_mux-20261101.6/PKG-INFO +203 -0
  4. py_screen_mux-20261101.6/README.md +180 -0
  5. py_screen_mux-20261101.6/deps/README.md +54 -0
  6. py_screen_mux-20261101.6/deps/libvterm/.bzrignore +13 -0
  7. py_screen_mux-20261101.6/deps/libvterm/CODE-MAP +81 -0
  8. py_screen_mux-20261101.6/deps/libvterm/CONTRIBUTING +22 -0
  9. py_screen_mux-20261101.6/deps/libvterm/LICENSE +23 -0
  10. py_screen_mux-20261101.6/deps/libvterm/Makefile +142 -0
  11. py_screen_mux-20261101.6/deps/libvterm/bin/unterm.c +280 -0
  12. py_screen_mux-20261101.6/deps/libvterm/bin/vterm-ctrl.c +354 -0
  13. py_screen_mux-20261101.6/deps/libvterm/bin/vterm-dump.c +245 -0
  14. py_screen_mux-20261101.6/deps/libvterm/doc/URLs +14 -0
  15. py_screen_mux-20261101.6/deps/libvterm/doc/seqs.txt +283 -0
  16. py_screen_mux-20261101.6/deps/libvterm/find-wide-chars.pl +30 -0
  17. py_screen_mux-20261101.6/deps/libvterm/include/vterm.h +637 -0
  18. py_screen_mux-20261101.6/deps/libvterm/include/vterm_keycodes.h +61 -0
  19. py_screen_mux-20261101.6/deps/libvterm/src/encoding/DECdrawing.inc +36 -0
  20. py_screen_mux-20261101.6/deps/libvterm/src/encoding/DECdrawing.tbl +31 -0
  21. py_screen_mux-20261101.6/deps/libvterm/src/encoding/uk.inc +6 -0
  22. py_screen_mux-20261101.6/deps/libvterm/src/encoding/uk.tbl +1 -0
  23. py_screen_mux-20261101.6/deps/libvterm/src/encoding.c +230 -0
  24. py_screen_mux-20261101.6/deps/libvterm/src/fullwidth.inc +111 -0
  25. py_screen_mux-20261101.6/deps/libvterm/src/keyboard.c +226 -0
  26. py_screen_mux-20261101.6/deps/libvterm/src/mouse.c +99 -0
  27. py_screen_mux-20261101.6/deps/libvterm/src/parser.c +402 -0
  28. py_screen_mux-20261101.6/deps/libvterm/src/pen.c +607 -0
  29. py_screen_mux-20261101.6/deps/libvterm/src/rect.h +56 -0
  30. py_screen_mux-20261101.6/deps/libvterm/src/screen.c +1191 -0
  31. py_screen_mux-20261101.6/deps/libvterm/src/state.c +2370 -0
  32. py_screen_mux-20261101.6/deps/libvterm/src/unicode.c +313 -0
  33. py_screen_mux-20261101.6/deps/libvterm/src/utf8.h +39 -0
  34. py_screen_mux-20261101.6/deps/libvterm/src/vterm.c +430 -0
  35. py_screen_mux-20261101.6/deps/libvterm/src/vterm_internal.h +297 -0
  36. py_screen_mux-20261101.6/deps/libvterm/t/02parser.test +266 -0
  37. py_screen_mux-20261101.6/deps/libvterm/t/03encoding_utf8.test +122 -0
  38. py_screen_mux-20261101.6/deps/libvterm/t/10state_putglyph.test +74 -0
  39. py_screen_mux-20261101.6/deps/libvterm/t/11state_movecursor.test +224 -0
  40. py_screen_mux-20261101.6/deps/libvterm/t/12state_scroll.test +156 -0
  41. py_screen_mux-20261101.6/deps/libvterm/t/13state_edit.test +304 -0
  42. py_screen_mux-20261101.6/deps/libvterm/t/14state_encoding.test +105 -0
  43. py_screen_mux-20261101.6/deps/libvterm/t/15state_mode.test +86 -0
  44. py_screen_mux-20261101.6/deps/libvterm/t/16state_resize.test +48 -0
  45. py_screen_mux-20261101.6/deps/libvterm/t/17state_mouse.test +191 -0
  46. py_screen_mux-20261101.6/deps/libvterm/t/18state_termprops.test +42 -0
  47. py_screen_mux-20261101.6/deps/libvterm/t/20state_wrapping.test +69 -0
  48. py_screen_mux-20261101.6/deps/libvterm/t/21state_tabstops.test +60 -0
  49. py_screen_mux-20261101.6/deps/libvterm/t/22state_save.test +64 -0
  50. py_screen_mux-20261101.6/deps/libvterm/t/25state_input.test +157 -0
  51. py_screen_mux-20261101.6/deps/libvterm/t/26state_query.test +63 -0
  52. py_screen_mux-20261101.6/deps/libvterm/t/27state_reset.test +32 -0
  53. py_screen_mux-20261101.6/deps/libvterm/t/28state_dbl_wh.test +61 -0
  54. py_screen_mux-20261101.6/deps/libvterm/t/29state_fallback.test +31 -0
  55. py_screen_mux-20261101.6/deps/libvterm/t/30state_pen.test +133 -0
  56. py_screen_mux-20261101.6/deps/libvterm/t/31state_rep.test +128 -0
  57. py_screen_mux-20261101.6/deps/libvterm/t/32state_flow.test +28 -0
  58. py_screen_mux-20261101.6/deps/libvterm/t/40state_selection.test +89 -0
  59. py_screen_mux-20261101.6/deps/libvterm/t/60screen_ascii.test +69 -0
  60. py_screen_mux-20261101.6/deps/libvterm/t/61screen_unicode.test +47 -0
  61. py_screen_mux-20261101.6/deps/libvterm/t/62screen_damage.test +155 -0
  62. py_screen_mux-20261101.6/deps/libvterm/t/63screen_resize.test +117 -0
  63. py_screen_mux-20261101.6/deps/libvterm/t/64screen_pen.test +71 -0
  64. py_screen_mux-20261101.6/deps/libvterm/t/65screen_protect.test +16 -0
  65. py_screen_mux-20261101.6/deps/libvterm/t/66screen_extent.test +11 -0
  66. py_screen_mux-20261101.6/deps/libvterm/t/67screen_dbl_wh.test +38 -0
  67. py_screen_mux-20261101.6/deps/libvterm/t/68screen_termprops.test +17 -0
  68. py_screen_mux-20261101.6/deps/libvterm/t/69screen_reflow.test +88 -0
  69. py_screen_mux-20261101.6/deps/libvterm/t/90vttest_01-movement-1.test +87 -0
  70. py_screen_mux-20261101.6/deps/libvterm/t/90vttest_01-movement-2.test +40 -0
  71. py_screen_mux-20261101.6/deps/libvterm/t/90vttest_01-movement-3.test +21 -0
  72. py_screen_mux-20261101.6/deps/libvterm/t/90vttest_01-movement-4.test +36 -0
  73. py_screen_mux-20261101.6/deps/libvterm/t/90vttest_02-screen-1.test +18 -0
  74. py_screen_mux-20261101.6/deps/libvterm/t/90vttest_02-screen-2.test +29 -0
  75. py_screen_mux-20261101.6/deps/libvterm/t/90vttest_02-screen-3.test +16 -0
  76. py_screen_mux-20261101.6/deps/libvterm/t/90vttest_02-screen-4.test +17 -0
  77. py_screen_mux-20261101.6/deps/libvterm/t/92lp1640917.test +13 -0
  78. py_screen_mux-20261101.6/deps/libvterm/t/harness.c +1235 -0
  79. py_screen_mux-20261101.6/deps/libvterm/t/run-test.pl +234 -0
  80. py_screen_mux-20261101.6/deps/libvterm/tbl2inc_c.pl +30 -0
  81. py_screen_mux-20261101.6/deps/libvterm/vterm.pc.in +8 -0
  82. py_screen_mux-20261101.6/pyproject.toml +51 -0
  83. py_screen_mux-20261101.6/setup.cfg +4 -0
  84. py_screen_mux-20261101.6/setup.py +24 -0
  85. py_screen_mux-20261101.6/src/py_screen/__init__.py +109 -0
  86. py_screen_mux-20261101.6/src/py_screen/__main__.py +3 -0
  87. py_screen_mux-20261101.6/src/py_screen/_vterm.c +932 -0
  88. py_screen_mux-20261101.6/src/py_screen/brain.py +1153 -0
  89. py_screen_mux-20261101.6/src/py_screen/client.py +97 -0
  90. py_screen_mux-20261101.6/src/py_screen/config.py +143 -0
  91. py_screen_mux-20261101.6/src/py_screen/keeper.py +280 -0
  92. py_screen_mux-20261101.6/src/py_screen/layout.py +219 -0
  93. py_screen_mux-20261101.6/src/py_screen/mouse.py +92 -0
  94. py_screen_mux-20261101.6/src/py_screen/proto.py +75 -0
  95. py_screen_mux-20261101.6/src/py_screen/screen.py +292 -0
  96. py_screen_mux-20261101.6/src/py_screen/term.py +167 -0
  97. py_screen_mux-20261101.6/src/py_screen/vterm.py +16 -0
  98. py_screen_mux-20261101.6/src/py_screen_mux.egg-info/PKG-INFO +203 -0
  99. py_screen_mux-20261101.6/src/py_screen_mux.egg-info/SOURCES.txt +108 -0
  100. py_screen_mux-20261101.6/src/py_screen_mux.egg-info/dependency_links.txt +1 -0
  101. py_screen_mux-20261101.6/src/py_screen_mux.egg-info/entry_points.txt +2 -0
  102. py_screen_mux-20261101.6/src/py_screen_mux.egg-info/requires.txt +1 -0
  103. py_screen_mux-20261101.6/src/py_screen_mux.egg-info/top_level.txt +1 -0
  104. py_screen_mux-20261101.6/tests/test_flood.py +106 -0
  105. py_screen_mux-20261101.6/tests/test_m0.py +102 -0
  106. py_screen_mux-20261101.6/tests/test_m1.py +160 -0
  107. py_screen_mux-20261101.6/tests/test_mouse.py +144 -0
  108. py_screen_mux-20261101.6/tests/test_parser.py +120 -0
  109. py_screen_mux-20261101.6/tests/test_redraw.py +97 -0
  110. py_screen_mux-20261101.6/tests/test_vterm.py +125 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cong Zhang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ # The sdist carries libvterm's sources and licence, so a wheel builds from it alone.
2
+ graft deps/libvterm
3
+ include deps/README.md
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-screen-mux
3
+ Version: 20261101.6
4
+ Summary: py-screen: a terminal workspace (multiplexer) of my own, with the pys command
5
+ Author-email: Cong Zhang <13283869+congzhangzh@users.noreply.github.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/congzhangzh/py-screen
8
+ Project-URL: Source, https://github.com/congzhangzh/py-screen
9
+ Project-URL: Releases, https://github.com/congzhangzh/py-screen/releases
10
+ Project-URL: Issues, https://github.com/congzhangzh/py-screen/issues
11
+ Classifier: Environment :: Console
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Programming Language :: C
16
+ Classifier: Topic :: Terminals :: Terminal Emulators/X Terminals
17
+ Requires-Python: >=3.14
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ License-File: deps/libvterm/LICENSE
21
+ Requires-Dist: pyte>=0.8.2
22
+ Dynamic: license-file
23
+
24
+ # py-screen
25
+
26
+ A terminal workspace of my own, in Python: not a clone of screen or tmux, built for fun,
27
+ made more powerful and improved by daily use. The design is in [`spec/`](https://github.com/congzhangzh/py-screen/tree/main/spec/); the
28
+ current version is [`spec/spec.v3.md`](https://github.com/congzhangzh/py-screen/blob/main/spec/spec.v3.md).
29
+
30
+ Status: **M1 in progress** (Linux; Windows pieces checked on CI). One server, many windows, attach/detach, hot reload, flood-safe typing.
31
+
32
+ ## How it works
33
+
34
+ ```
35
+ client (pys attach) ──► brain (Python, restartable) ──► keeper (tiny, stable) ──► ptys
36
+ ```
37
+
38
+ - **keeper** owns every pty and child process and is the only reader of their output
39
+ (a byte pump with a ring buffer per window). It restarts the brain when it exits.
40
+ - **brain** holds a pyte screen per window, renders the current one plus a status line,
41
+ handles keys and answers RPCs. `pys reload` restarts it; shells never notice, because
42
+ the ptys live in the keeper. The new brain replays the ring buffers, then nudges each pty
43
+ (SIGWINCH) so full-screen apps repaint.
44
+ - **client** puts the terminal in raw mode, forwards keys and size, prints frames, and
45
+ reconnects by itself across a brain reload.
46
+
47
+ ### Floods: the screen model is lossless
48
+
49
+ Every byte a window writes is parsed, in order, whether the window is shown or hidden,
50
+ attached or not. That matters most for apps that draw by diff, like Claude Code: they
51
+ write only the cells they think changed and never rewrite a cell they think is right, so
52
+ a single lost byte leaves wrong cells for good (and a lost mode switch, such as the
53
+ alternate screen or bracketed paste, breaks input too). Until 2026-11-01 py-screen parsed
54
+ only the tail of a flood and dropped a hidden window's output past 128 KB; that is what
55
+ left stray characters in Claude's status line.
56
+
57
+ Parsing is the slow part (pyte in Python; py-screen's fast paths for plain text, line
58
+ feeds and CSI sequences take it from 0.85 to 1.7 MB/s of scrolling text), so:
59
+
60
+ - **Backpressure, not dropping.** The keeper gives each window a **credit** of 256 KB
61
+ un-acked output and stops reading that pty past it. The brain acks bytes only once it
62
+ has parsed them, so a flooding app simply waits on its write, as under GNU screen. One
63
+ window can't fill the shared stream, so another window's echo never waits behind it.
64
+ - **Small steps.** The brain parses in steps of 2 ms (2 KB per window per turn, shown
65
+ windows first, the others round robin), then goes back to keys and frames.
66
+ - **Keys first.** For 100 ms after a key, only shown windows are parsed: nothing else
67
+ stands between a key and its echo (about 2 ms median while a hidden window floods).
68
+
69
+ A flood runs at the parser's speed. With the vterm engine (libvterm, in C) that is faster
70
+ than GNU screen: `seq 1 5000000` takes 1.6 s against screen's 13.4 s, a 50 MB coloured log
71
+ 0.9 s against 8.1 s ([`bench/README.md`](https://github.com/congzhangzh/py-screen/blob/main/bench/README.md) has the history).
72
+
73
+ **Screen engines.** Each window's screen is a `term.Term`; `PYS_TERM` picks the engine:
74
+
75
+ | Engine | What | When |
76
+ |---|---|---|
77
+ | `vterm` (default) | [libvterm](https://github.com/congzhangzh/py-screen/blob/main/deps/README.md)'s parser and state, compiled in, with py-screen's own screen store | When the C extension was built (needs a C compiler at install; release wheels have it) |
78
+ | `pyte` | [pyte](https://github.com/selectel/pyte) with pure-Python fast paths | Always there; about 20× slower on floods |
79
+
80
+ `PYS_TERM=vterm` makes a missing extension an error instead of a quiet fallback.
81
+
82
+ A sequence that pyte can't apply (an extra parameter, a `?` where none is defined, an
83
+ unknown erase value) is ignored, as terminals do; the bytes after it are still parsed.
84
+
85
+ ## Releases
86
+
87
+ Rolling releases: every push to `main` that passes is published as `<YYYYMMDD>.<N>`, the
88
+ version and the tag alike, on [PyPI as `py-screen-mux`](https://pypi.org/project/py-screen-mux/)
89
+ and as a [GitHub release](https://github.com/congzhangzh/py-screen/releases), once, never
90
+ replaced. Pushes that change only docs, specs or bench results publish nothing. A release must pass, in [`release.yml`](https://github.com/congzhangzh/py-screen/blob/main/.github/workflows/release.yml):
91
+
92
+ - the suite on Linux and macOS, on both screen engines (vterm and pyte);
93
+ - the vterm engine built with ASan + UBSan and fuzzed against libvterm's own screen
94
+ (a long run every night, in [`sanitize.yml`](https://github.com/congzhangzh/py-screen/blob/main/.github/workflows/sanitize.yml));
95
+ - a parse-speed check: vterm's lead over pyte must keep at least half of the newest
96
+ recorded run's (`release/bench_gate.py`, against `bench/results/parse-*.json`);
97
+ - each wheel, built by cibuildwheel from the sdist, installed and smoke-tested on the vterm
98
+ engine on its own system: Linux x86_64 and aarch64 (manylinux), macOS arm64 and x86_64.
99
+
100
+ Install with [uv](https://docs.astral.sh/uv/), which fetches Python 3.14 if needed and picks
101
+ the wheel for your system:
102
+
103
+ ```bash
104
+ uv tool install py-screen-mux # the command is pys
105
+ ```
106
+
107
+ The package is `py-screen-mux` on PyPI because `py-screen` is too close to an existing
108
+ `pyscreen` there; the import is `py_screen` and the command `pys`.
109
+
110
+ Elsewhere, the sdist builds with a C compiler, or runs the pure-Python pyte engine without one.
111
+ Windows joins once its platform layer lands.
112
+
113
+ ## Use
114
+
115
+ ```bash
116
+ uv tool install -e . # puts `pys` in ~/.local/bin, running this checkout (edits apply on reload)
117
+ pys # attach (starts the server on first use)
118
+ pys ls [--json] # windows
119
+ pys new [--select] [-- cmd …]
120
+ pys send 0 'make test' --enter
121
+ pys capture 0 # the window's screen as text
122
+ pys reload # new brain, same shells
123
+ pys kill # end everything
124
+ ```
125
+
126
+ Keys: the prefix is **`C-a`**, or **`C-]`** when you run `pys` inside GNU screen (`$STY`
127
+ set), so the two don't fight. Override with `PREFIX` in the config or `PYS_PREFIX`.
128
+
129
+ | Keys | Action |
130
+ |---|---|
131
+ | `c` / `C-c` | new window in the focused window's directory |
132
+ | `\|` / `-` | split side by side / stacked, with a new window in the same dir |
133
+ | `\` / `_` | empty split side by side / stacked |
134
+ | `h` `j` `k` `l` | move the focus between panes |
135
+ | `r` then `h/l/j/k`, `=` | resize mode (any other key ends it) · `=` equalise panes |
136
+ | `x` / `X` | close the pane (window keeps running) / kill the window |
137
+ | `n` `p` `0`–`9`, `Alt-0`–`Alt-9` | next, previous, select |
138
+ | `Space` or the prefix twice | toggle to the last window |
139
+ | `w` `"` `F2` | window list (`j/k`, arrows, digits, Enter, Esc) |
140
+ | `,` | rename the window |
141
+ | `?` | help: the windows (a digit or a click selects one), these keys, your config's bindings; Space pages when it does not fit |
142
+ | `a` | send a literal prefix |
143
+ | `d` · `C-l` · `R` · `Q` | detach · redraw · reload the brain · quit everything |
144
+
145
+ Without the prefix: `Alt-0`…`Alt-9` select, `F2` opens the list, **`Alt-Tab` /
146
+ `Alt-Shift-Tab`** go to the next / previous window. Desktops usually keep Alt-Tab for
147
+ themselves (GNOME: `org.gnome.desktop.wm.keybindings switch-applications`), so it reaches
148
+ py-screen only where the desktop lets it through, e.g. over ssh from another machine.
149
+ Inside GNU screen, screen's own `bindkey` lines may also take the Alt keys first.
150
+
151
+ ### Mouse
152
+
153
+ On by default, like tmux's `mouse on`:
154
+
155
+ | Mouse | Does |
156
+ |-------|------|
157
+ | click a pane | focus it; its app gets the click too if it asked for the mouse |
158
+ | click a window in the status line | select it |
159
+ | drag a `│` separator or a caption line | resize the panes on both sides |
160
+ | wheel | goes to the pane under the pointer, focused or not; a full-screen app that didn't ask for the mouse (less, man) gets arrow keys |
161
+ | Shift+drag | select text in your terminal (most terminals; the mouse is pys's otherwise) |
162
+
163
+ Every report is routed to the pane under the pointer and rewritten in that pane's
164
+ coordinates, in the encoding the app asked for (SGR 1006, or the old X10 form). A press
165
+ grabs its pane: the drag and the release go there even if the pointer leaves it.
166
+ `MOUSE = False` in the config gives plain drags back to the terminal; the mouse is then
167
+ pys's only while the focused app asks for it (still translated per pane).
168
+
169
+ ### Config
170
+
171
+ `~/.config/pys/config.py` (plain Python, read on every brain start, so `C-a R` applies an
172
+ edit): status widgets and extra key bindings. See `src/py_screen/config.py` for the helpers.
173
+
174
+ ```python
175
+ WIDGETS = [cmd(["git", "-C", "{cwd}", "branch", "--show-current"], every=10), load(), clock()]
176
+ BIND = {"T": ("top", ["htop"])} # C-a T opens a window running htop
177
+ MOUSE = True # False: plain drags select text in the terminal
178
+ ```
179
+
180
+ The layout (splits, focus, renamed titles) is saved on every change, so a reload keeps it.
181
+
182
+ Sockets live in `$XDG_RUNTIME_DIR/pys/` (`PYS_DIR` overrides; keep it short: an AF_UNIX
183
+ path is limited to 108 bytes). Server log: `server.log` in that dir.
184
+
185
+ ## Test and measure
186
+
187
+ ```bash
188
+ uv run pytest -s # M0 done-criteria (spec §6)
189
+ uv run python bench/m0_speed.py --work <scratch dir> # speed vs GNU screen (spec §8.1)
190
+ ```
191
+
192
+ Results of each run are kept in [`bench/results/`](https://github.com/congzhangzh/py-screen/tree/main/bench/results/), one file per run, and [`bench/README.md`](https://github.com/congzhangzh/py-screen/blob/main/bench/README.md) tells how the speed evolved, one row per run.
193
+
194
+ ## Known gaps (M1)
195
+
196
+ - A mouse report split across two reads is passed on as keys (rare; the outer terminal
197
+ writes each report at once).
198
+ - After a brain restart the screens are rebuilt from the keeper's last 2 MB per window; if
199
+ that ring wrapped, older state is gone and full-screen apps get a `SIGWINCH` to repaint.
200
+ Keeping the screens across a planned reload is open.
201
+ - Floods are bounded by pyte's speed (see "Floods").
202
+ - No scrollback view yet (M2).
203
+ - Linux (and macOS) only for now; the Windows pieces are checked by the manual CI run.
@@ -0,0 +1,180 @@
1
+ # py-screen
2
+
3
+ A terminal workspace of my own, in Python: not a clone of screen or tmux, built for fun,
4
+ made more powerful and improved by daily use. The design is in [`spec/`](https://github.com/congzhangzh/py-screen/tree/main/spec/); the
5
+ current version is [`spec/spec.v3.md`](https://github.com/congzhangzh/py-screen/blob/main/spec/spec.v3.md).
6
+
7
+ Status: **M1 in progress** (Linux; Windows pieces checked on CI). One server, many windows, attach/detach, hot reload, flood-safe typing.
8
+
9
+ ## How it works
10
+
11
+ ```
12
+ client (pys attach) ──► brain (Python, restartable) ──► keeper (tiny, stable) ──► ptys
13
+ ```
14
+
15
+ - **keeper** owns every pty and child process and is the only reader of their output
16
+ (a byte pump with a ring buffer per window). It restarts the brain when it exits.
17
+ - **brain** holds a pyte screen per window, renders the current one plus a status line,
18
+ handles keys and answers RPCs. `pys reload` restarts it; shells never notice, because
19
+ the ptys live in the keeper. The new brain replays the ring buffers, then nudges each pty
20
+ (SIGWINCH) so full-screen apps repaint.
21
+ - **client** puts the terminal in raw mode, forwards keys and size, prints frames, and
22
+ reconnects by itself across a brain reload.
23
+
24
+ ### Floods: the screen model is lossless
25
+
26
+ Every byte a window writes is parsed, in order, whether the window is shown or hidden,
27
+ attached or not. That matters most for apps that draw by diff, like Claude Code: they
28
+ write only the cells they think changed and never rewrite a cell they think is right, so
29
+ a single lost byte leaves wrong cells for good (and a lost mode switch, such as the
30
+ alternate screen or bracketed paste, breaks input too). Until 2026-11-01 py-screen parsed
31
+ only the tail of a flood and dropped a hidden window's output past 128 KB; that is what
32
+ left stray characters in Claude's status line.
33
+
34
+ Parsing is the slow part (pyte in Python; py-screen's fast paths for plain text, line
35
+ feeds and CSI sequences take it from 0.85 to 1.7 MB/s of scrolling text), so:
36
+
37
+ - **Backpressure, not dropping.** The keeper gives each window a **credit** of 256 KB
38
+ un-acked output and stops reading that pty past it. The brain acks bytes only once it
39
+ has parsed them, so a flooding app simply waits on its write, as under GNU screen. One
40
+ window can't fill the shared stream, so another window's echo never waits behind it.
41
+ - **Small steps.** The brain parses in steps of 2 ms (2 KB per window per turn, shown
42
+ windows first, the others round robin), then goes back to keys and frames.
43
+ - **Keys first.** For 100 ms after a key, only shown windows are parsed: nothing else
44
+ stands between a key and its echo (about 2 ms median while a hidden window floods).
45
+
46
+ A flood runs at the parser's speed. With the vterm engine (libvterm, in C) that is faster
47
+ than GNU screen: `seq 1 5000000` takes 1.6 s against screen's 13.4 s, a 50 MB coloured log
48
+ 0.9 s against 8.1 s ([`bench/README.md`](https://github.com/congzhangzh/py-screen/blob/main/bench/README.md) has the history).
49
+
50
+ **Screen engines.** Each window's screen is a `term.Term`; `PYS_TERM` picks the engine:
51
+
52
+ | Engine | What | When |
53
+ |---|---|---|
54
+ | `vterm` (default) | [libvterm](https://github.com/congzhangzh/py-screen/blob/main/deps/README.md)'s parser and state, compiled in, with py-screen's own screen store | When the C extension was built (needs a C compiler at install; release wheels have it) |
55
+ | `pyte` | [pyte](https://github.com/selectel/pyte) with pure-Python fast paths | Always there; about 20× slower on floods |
56
+
57
+ `PYS_TERM=vterm` makes a missing extension an error instead of a quiet fallback.
58
+
59
+ A sequence that pyte can't apply (an extra parameter, a `?` where none is defined, an
60
+ unknown erase value) is ignored, as terminals do; the bytes after it are still parsed.
61
+
62
+ ## Releases
63
+
64
+ Rolling releases: every push to `main` that passes is published as `<YYYYMMDD>.<N>`, the
65
+ version and the tag alike, on [PyPI as `py-screen-mux`](https://pypi.org/project/py-screen-mux/)
66
+ and as a [GitHub release](https://github.com/congzhangzh/py-screen/releases), once, never
67
+ replaced. Pushes that change only docs, specs or bench results publish nothing. A release must pass, in [`release.yml`](https://github.com/congzhangzh/py-screen/blob/main/.github/workflows/release.yml):
68
+
69
+ - the suite on Linux and macOS, on both screen engines (vterm and pyte);
70
+ - the vterm engine built with ASan + UBSan and fuzzed against libvterm's own screen
71
+ (a long run every night, in [`sanitize.yml`](https://github.com/congzhangzh/py-screen/blob/main/.github/workflows/sanitize.yml));
72
+ - a parse-speed check: vterm's lead over pyte must keep at least half of the newest
73
+ recorded run's (`release/bench_gate.py`, against `bench/results/parse-*.json`);
74
+ - each wheel, built by cibuildwheel from the sdist, installed and smoke-tested on the vterm
75
+ engine on its own system: Linux x86_64 and aarch64 (manylinux), macOS arm64 and x86_64.
76
+
77
+ Install with [uv](https://docs.astral.sh/uv/), which fetches Python 3.14 if needed and picks
78
+ the wheel for your system:
79
+
80
+ ```bash
81
+ uv tool install py-screen-mux # the command is pys
82
+ ```
83
+
84
+ The package is `py-screen-mux` on PyPI because `py-screen` is too close to an existing
85
+ `pyscreen` there; the import is `py_screen` and the command `pys`.
86
+
87
+ Elsewhere, the sdist builds with a C compiler, or runs the pure-Python pyte engine without one.
88
+ Windows joins once its platform layer lands.
89
+
90
+ ## Use
91
+
92
+ ```bash
93
+ uv tool install -e . # puts `pys` in ~/.local/bin, running this checkout (edits apply on reload)
94
+ pys # attach (starts the server on first use)
95
+ pys ls [--json] # windows
96
+ pys new [--select] [-- cmd …]
97
+ pys send 0 'make test' --enter
98
+ pys capture 0 # the window's screen as text
99
+ pys reload # new brain, same shells
100
+ pys kill # end everything
101
+ ```
102
+
103
+ Keys: the prefix is **`C-a`**, or **`C-]`** when you run `pys` inside GNU screen (`$STY`
104
+ set), so the two don't fight. Override with `PREFIX` in the config or `PYS_PREFIX`.
105
+
106
+ | Keys | Action |
107
+ |---|---|
108
+ | `c` / `C-c` | new window in the focused window's directory |
109
+ | `\|` / `-` | split side by side / stacked, with a new window in the same dir |
110
+ | `\` / `_` | empty split side by side / stacked |
111
+ | `h` `j` `k` `l` | move the focus between panes |
112
+ | `r` then `h/l/j/k`, `=` | resize mode (any other key ends it) · `=` equalise panes |
113
+ | `x` / `X` | close the pane (window keeps running) / kill the window |
114
+ | `n` `p` `0`–`9`, `Alt-0`–`Alt-9` | next, previous, select |
115
+ | `Space` or the prefix twice | toggle to the last window |
116
+ | `w` `"` `F2` | window list (`j/k`, arrows, digits, Enter, Esc) |
117
+ | `,` | rename the window |
118
+ | `?` | help: the windows (a digit or a click selects one), these keys, your config's bindings; Space pages when it does not fit |
119
+ | `a` | send a literal prefix |
120
+ | `d` · `C-l` · `R` · `Q` | detach · redraw · reload the brain · quit everything |
121
+
122
+ Without the prefix: `Alt-0`…`Alt-9` select, `F2` opens the list, **`Alt-Tab` /
123
+ `Alt-Shift-Tab`** go to the next / previous window. Desktops usually keep Alt-Tab for
124
+ themselves (GNOME: `org.gnome.desktop.wm.keybindings switch-applications`), so it reaches
125
+ py-screen only where the desktop lets it through, e.g. over ssh from another machine.
126
+ Inside GNU screen, screen's own `bindkey` lines may also take the Alt keys first.
127
+
128
+ ### Mouse
129
+
130
+ On by default, like tmux's `mouse on`:
131
+
132
+ | Mouse | Does |
133
+ |-------|------|
134
+ | click a pane | focus it; its app gets the click too if it asked for the mouse |
135
+ | click a window in the status line | select it |
136
+ | drag a `│` separator or a caption line | resize the panes on both sides |
137
+ | wheel | goes to the pane under the pointer, focused or not; a full-screen app that didn't ask for the mouse (less, man) gets arrow keys |
138
+ | Shift+drag | select text in your terminal (most terminals; the mouse is pys's otherwise) |
139
+
140
+ Every report is routed to the pane under the pointer and rewritten in that pane's
141
+ coordinates, in the encoding the app asked for (SGR 1006, or the old X10 form). A press
142
+ grabs its pane: the drag and the release go there even if the pointer leaves it.
143
+ `MOUSE = False` in the config gives plain drags back to the terminal; the mouse is then
144
+ pys's only while the focused app asks for it (still translated per pane).
145
+
146
+ ### Config
147
+
148
+ `~/.config/pys/config.py` (plain Python, read on every brain start, so `C-a R` applies an
149
+ edit): status widgets and extra key bindings. See `src/py_screen/config.py` for the helpers.
150
+
151
+ ```python
152
+ WIDGETS = [cmd(["git", "-C", "{cwd}", "branch", "--show-current"], every=10), load(), clock()]
153
+ BIND = {"T": ("top", ["htop"])} # C-a T opens a window running htop
154
+ MOUSE = True # False: plain drags select text in the terminal
155
+ ```
156
+
157
+ The layout (splits, focus, renamed titles) is saved on every change, so a reload keeps it.
158
+
159
+ Sockets live in `$XDG_RUNTIME_DIR/pys/` (`PYS_DIR` overrides; keep it short: an AF_UNIX
160
+ path is limited to 108 bytes). Server log: `server.log` in that dir.
161
+
162
+ ## Test and measure
163
+
164
+ ```bash
165
+ uv run pytest -s # M0 done-criteria (spec §6)
166
+ uv run python bench/m0_speed.py --work <scratch dir> # speed vs GNU screen (spec §8.1)
167
+ ```
168
+
169
+ Results of each run are kept in [`bench/results/`](https://github.com/congzhangzh/py-screen/tree/main/bench/results/), one file per run, and [`bench/README.md`](https://github.com/congzhangzh/py-screen/blob/main/bench/README.md) tells how the speed evolved, one row per run.
170
+
171
+ ## Known gaps (M1)
172
+
173
+ - A mouse report split across two reads is passed on as keys (rare; the outer terminal
174
+ writes each report at once).
175
+ - After a brain restart the screens are rebuilt from the keeper's last 2 MB per window; if
176
+ that ring wrapped, older state is gone and full-screen apps get a `SIGWINCH` to repaint.
177
+ Keeping the screens across a planned reload is open.
178
+ - Floods are bounded by pyte's speed (see "Floods").
179
+ - No scrollback view yet (M2).
180
+ - Linux (and macOS) only for now; the Windows pieces are checked by the manual CI run.
@@ -0,0 +1,54 @@
1
+ # Third-party code
2
+
3
+ ## libvterm 0.3.3 (`deps/libvterm`, MIT, see its `LICENSE`)
4
+
5
+ The terminal state machine behind the `vterm` screen engine. Upstream lives in
6
+ Bazaar on Launchpad; this tree comes from the git mirror Neovim kept of it.
7
+
8
+ | What | Value |
9
+ |---|---|
10
+ | Added with | `git read-tree --prefix=deps/libvterm -u v0.3.3^{tree}` after fetching the tag: one plain commit, no merge, so history stays linear; its `git-subtree-dir:` / `git-subtree-split:` trailers are what `git subtree` reads |
11
+ | Upstream commit | `9d6d2112335080312ef8c36667fa717ded4f7daf` (tag `v0.3.3`) |
12
+ | Release tarball | `libvterm-0.3.3.tar.gz`, sha256 `09156f43dd2128bd347cbeebe50d9a571d32c64e0cf18d211197946aff7226e0`, the same file as Debian's `libvterm_0.3.3.orig.tar.gz` |
13
+ | Checked | `src/` and `include/` equal the tarball's, file for file |
14
+ | Tag vs tarball | The tag also has `doc/`, `CODE-MAP`, the `.tbl` sources and `tbl2inc_c.pl`; its `Makefile` and `vterm.pc.in` differ (the tarball strips the dist section); `t/69screen_sb_clear.test` is in the tarball only; the tarball has the generated `.inc` files (change 1) |
15
+
16
+ Changes on top of upstream, each its own commit (`git log -- deps/libvterm`):
17
+
18
+ 1. `src/encoding/DECdrawing.inc` and `uk.inc`: generated by upstream's Makefile from the
19
+ `.tbl` files with Perl; copied from the release tarball, which ships them, so a build
20
+ needs only a C compiler.
21
+
22
+ 2. `src/state.c`, REP (`CSI n b`, repeat the previous character): with no character drawn
23
+ yet, `combine_width` is 0 and `while (pos.col < col) pos.col += combine_width` never
24
+ ends, so one `ESC [ 3 b` as a terminal's first output froze the brain for good (no
25
+ crash, so no restart). Now REP does nothing then, as ECMA-48 leaves it undefined.
26
+ Found by tests/test_vterm.py on its second seed; libvterm's own screen layer hangs
27
+ the same way. The same loop repeated a wide character into the last column, half
28
+ past the row's end: libvterm's screen layer then writes through a NULL cell pointer
29
+ (`screen.c` putglyph, a segfault; found on seed 46). Now a character is repeated
30
+ only while it fits. Not reported upstream yet.
31
+
32
+ 3. `src/state.c`, `on_resize`: only the bottom and right margins were clamped to the new
33
+ size, so a scroll region that no longer fit (`ESC [ 6 ; 10 r`, then 3 rows) ended
34
+ upside down, and the next line feed's `scroll()` did a `memmove` of negative size
35
+ (found by AddressSanitizer on tests/test_vterm.py seed 10040; before it, a heap
36
+ corruption). Such a region now goes back to the whole screen, as xterm resets
37
+ margins on a resize. Not reported upstream yet.
38
+
39
+ 4. `src/state.c`, `savecursor` (restore, `ESC 8`, leaving `?1049`): the saved position
40
+ came back unclamped, so a cursor saved on row 4 and restored after a resize to 3 rows
41
+ sat off the screen; the next glyph read and wrote `lineinfo` past its end, and
42
+ libvterm's screen layer wrote through a NULL cell (AddressSanitizer, seed 103772
43
+ of a 5000-seed run). Clamped to the screen now, as xterm restores a cursor. Not
44
+ reported upstream yet.
45
+
46
+ Nothing else in libvterm is changed; py-screen's own C code reaches libvterm through its
47
+ headers and, for its own resize, the line-info arrays and allocator libvterm's state
48
+ expects a resize callback to replace (as `screen.c` does).
49
+
50
+ To update, keep it flat the same way: fetch the new tag, `git rm -rq deps/libvterm`,
51
+ `git read-tree --prefix=deps/libvterm -u <tag>^{tree}`, commit with the two trailers
52
+ naming the new upstream commit, then redo change 1 from that release's tarball and
53
+ check its sha256 against Debian's `.dsc`. (`git subtree pull --squash` also works from
54
+ the trailers, tested 0.3.2 → 0.3.3, but it adds a merge commit.)
@@ -0,0 +1,13 @@
1
+ .libs
2
+ *.lo
3
+ *.la
4
+
5
+ bin/*
6
+ !bin/*.c
7
+
8
+ pangoterm
9
+ t/test
10
+ t/suites.h
11
+ t/externs.h
12
+ t/harness
13
+ src/encoding/*.inc
@@ -0,0 +1,81 @@
1
+ bin/
2
+ - contains some standalone programs
3
+
4
+ bin/unterm.c
5
+ - an example program using libvterm to reconstruct the final state of a
6
+ terminal after replaying captured input
7
+
8
+ bin/vterm-ctrl.c
9
+ - a helper program that emits sequences to control a running libvterm
10
+ terminal
11
+
12
+ bin/vterm-dump.c
13
+ - an example program using the parser layer of libvterm to interpret captured
14
+ input
15
+
16
+ CODE-MAP
17
+ - high-level list and description of files in the repository
18
+
19
+ CONTRIBUTING
20
+ - documentation explaining how developers can contribute fixes and features
21
+
22
+ doc/
23
+ - contains documentation
24
+
25
+ doc/seqs.txt
26
+ - documents the sequences recognised by the library
27
+
28
+ include/vterm.h
29
+ - main include file
30
+
31
+ include/vterm_keycodes.h
32
+ - include file containing the keyboard input keycode enumerations
33
+
34
+ LICENSE
35
+ - legalese
36
+
37
+ Makefile
38
+ - main build file
39
+
40
+ src/
41
+ - contains the source code for the library
42
+
43
+ src/encoding.c
44
+ - handles mapping ISO/IEC 2022 alternate character sets into Unicode
45
+ codepoints
46
+
47
+ src/keyboard.c
48
+ - handles sending reported keyboard events to the output stream
49
+
50
+ src/mouse.c
51
+ - handles sending reported mouse events to the output stream
52
+
53
+ src/parser.c
54
+ - parses bytes from the input stream into parser-level events
55
+
56
+ src/pen.c
57
+ - interprets SGR sequences and maintains current rendering attributes
58
+
59
+ src/screen.c
60
+ - uses state-level events to maintain a buffer of current screen contents
61
+
62
+ src/state.c
63
+ - follows parser-level events to keep track of the overall terminal state
64
+
65
+ src/unicode.c
66
+ - utility functions for Unicode and UTF-8 handling
67
+
68
+ src/vterm.c
69
+ - toplevel object state and miscellaneous functions
70
+
71
+ src/vterm_internal.h
72
+ - include file for definitions private to the library's internals
73
+
74
+ t/
75
+ - contains unit tests
76
+
77
+ t/harness.c
78
+ - standalone program to embed the library into for unit-test purposes
79
+
80
+ t/run-test.pl
81
+ - invokes the test harness to run a single unit test script
@@ -0,0 +1,22 @@
1
+ How to Contribute
2
+ -----------------
3
+
4
+ The main resources for this library are:
5
+
6
+ Launchpad
7
+ https://launchpad.net/libvterm
8
+
9
+ IRC:
10
+ ##tty or #tickit on irc.libera.chat
11
+
12
+ Email:
13
+ Paul "LeoNerd" Evans <leonerd@leonerd.org.uk>
14
+
15
+
16
+ Bug reports and feature requests can be sent to any of the above resources.
17
+
18
+ New features, bug patches, etc.. should in the first instance be discussed via
19
+ any of the resources listed above, before starting work on the actual code.
20
+ There may be future plans or development already in-progress that could be
21
+ affected so it is better to discuss the ideas first before starting work
22
+ actually writing any code.
@@ -0,0 +1,23 @@
1
+
2
+
3
+ The MIT License
4
+
5
+ Copyright (c) 2008 Paul Evans <leonerd@leonerd.org.uk>
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in
15
+ all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ THE SOFTWARE.