Metadata-Version: 2.5
Name: termdesk
Version: 0.3.1
Summary: A remote desktop inside a kitty-protocol terminal pane, driven over VNC
License-Expression: MIT
License-File: LICENSE
Keywords: ghostty,kitty,remote-desktop,terminal,vnc
Classifier: Environment :: Console
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: System :: Networking
Requires-Python: >=3.9
Requires-Dist: numpy>=1.20
Requires-Dist: pillow>=9
Description-Content-Type: text/markdown

# termdesk

A full desktop, running inside a terminal pane.

termdesk is a VNC client that draws the remote screen with the kitty graphics
protocol and sends your keyboard and mouse back over RFB. Point it at any VNC
server and you get a real Linux desktop, or a Windows or macOS one, next to
your shell. Same idea as [terminal-browser](https://terminal-browser.com), which
does this for a headless Chromium, but for whole computers.

```
frames:  VNC server ──ZRLE rects──▶ termdesk ──kitty graphics (shm or pty)──▶ terminal
input:   terminal ──kitty keyboard protocol + pixel mouse──▶ termdesk ──RFB events──▶ VNC server
```

## Install

Needs Python 3.9+ and a terminal that speaks the kitty graphics protocol:
kitty, Ghostty, or WezTerm. The install is about 35 MB, almost all of it
numpy and Pillow; termdesk itself is one 25 KB file.

```bash
curl -fsSL https://tdesk-sh.vercel.app/install | bash
```

The script installs uv if you do not have it, then termdesk as a uv tool
from the wheel on the site, and drops the Claude Code skill into
`~/.claude/skills` when that directory exists. Upgrade by running it again.

## Use

```bash
termdesk host[:port]        # port defaults to 5900
termdesk <sandbox-name>     # or a sandbox from `termdesk sandbox up`
```

Ctrl+Q quits. Every other key, including modifiers, key repeat and release,
goes to the remote machine. Mouse motion, all buttons and the wheel are
forwarded with pixel precision.

Only password-less VNC is spoken, so put the server behind ssh:

```bash
ssh -L 5900:localhost:5900 user@box   # in one pane
termdesk localhost                     # in another
```

## Try it with the bundled desktop

`Dockerfile` builds an XFCE desktop with Firefox, a terminal, a file manager
and a text editor, served by TigerVNC on port 5900 with no password, bound to
localhost only.

```bash
docker compose up -d      # or, without the compose plugin:
docker build -t termdesk-desktop . && docker run -d --name termdesk-desktop \
  -p 127.0.0.1:5900:5900 --shm-size 512m termdesk-desktop
termdesk localhost
```

`tiny.Dockerfile` is a 150 MB Xvfb + x11vnc + xterm desktop for quick tests.

## Let an agent drive it

Every session exposes a control socket. `termdesk action` drives it from any
shell, so Claude Code or any other agent can use a real desktop while you
watch in the pane. An AGENT ACTING badge shows in the status line and you can
take the mouse at any time.

```bash
termdesk action state                     # accessibility tree as indexed elements (diff by default)
termdesk action click 11                  # act by element index ...
termdesk action click 640 400             # ... or by remote desktop pixel
termdesk action set-value 11 example.org  # click, select all, type
termdesk action key Return
termdesk action wait-idle                 # block until the screen settles
termdesk action screenshot /tmp/s.png     # pixels when the tree is not enough
termdesk action record start run.gif      # ... record stop writes a gif (mp4 with ffmpeg)
termdesk action done                      # clear the badge
termdesk setup                            # install the Claude Code skill
```

The skill file (`termdesk/skill/SKILL.md`) is the full contract an agent
follows: the command reference, the observe-act-verify loop, index stability,
and a confirmation policy for consequential actions. Any agent that can run
shell commands can use it; nothing is tied to one model.

`termdesk <target> --headless --daemon --name x` runs a session with no pane
at all, for CI or agents on a server.

## Disposable sandboxes

```bash
termdesk sandbox up --name work           # XFCE + Firefox container, prints localhost:<port>
termdesk work                             # show it
termdesk sandbox exec work -- firefox https://example.com
termdesk sandbox snapshot work clean      # docker commit
termdesk sandbox restore clean --name work2
termdesk sandbox ls / down work / down --all
```

Sandboxes run with a memory and CPU cap, a localhost-only port, and the AT-SPI
accessibility bus enabled. That is what makes `termdesk action state` work:
every visible element of XFCE and Firefox comes back with a stable index, role,
name, value, pixel rect and state, so an agent clicks `[11]` instead of
guessing coordinates from a screenshot.

## Running it for other people

The desktop container is stateless and shared, so a public box is a shared
whiteboard: everyone connected sees and drives the same screen. To host one:

1. Put the compose file on a VPS and `docker compose up -d`. The port stays on
   127.0.0.1.
2. Give people ssh access to a locked-down user whose only job is the tunnel
   (`command="/bin/false"` with `permitopen="localhost:5900"` in
   `authorized_keys` works), or a single shared key if you do not care who
   connects.
3. They run the two commands above.

One container per visitor, with a small orchestrator handing out ports, is the
version where people get private desktops. That is not in this repo.

## Why it is not laggy

- **Only changed pixels move.** The server sends damaged rectangles; termdesk
  composites each one into the on-screen image with a kitty animation-frame
  edit (`a=f`) instead of retransmitting the whole screen. A blinking cursor is
  a 16x16 patch of about 80 bytes on the pty.
- **No client-side scaling.** The image is placed over a column/row box and the
  terminal scales it on the GPU. Mouse coordinates are mapped back through the
  same ratio.
- **Shared memory when local.** If the terminal is on the same machine, pixels
  go through POSIX shared memory and only the segment name crosses the pty.
  Over ssh they are zlib-compressed raw RGB, which the terminal inflates
  natively.
- **ZRLE on the wire.** The RFB ZRLE encoding is negotiated first (with Zlib
  and CopyRect as fallbacks), decoded with numpy. A full 1280x800 XFCE frame
  is 63 KB instead of 4 MB raw and decodes in about 18 ms. Ten minutes of
  clicking around XFCE, Firefox and Thunar measured 2.7 MB inbound in total.
- **Nothing blocks input.** One `select` loop drains every pending server
  message before rendering, coalesces mouse motion to the latest position, and
  caps rendering at `--fps` (default 60).

The status line shows fps, bytes in and out, transport, and patch count.

## Limitations

- No VNC authentication yet. Use an ssh tunnel.
- `termdesk action state` only works in the bundled sandbox; other VNC hosts
  get screenshots and coordinate actions only.
- No RDP.
- The desktop image is shared between everyone who connects to it.
- Terminals without the kitty graphics protocol show nothing. Terminals
  without animation-frame support fall back to full-frame updates when they
  report an error.

## Development

```bash
python3 termdesk.py localhost --log /tmp/termdesk.log   # per-frame stats
docker build -f tiny.Dockerfile -t tiny-desktop . && docker run -d -p 5900:5900 tiny-desktop
```

MIT licensed.
