feat: switch from ttyd to real kitty via Xpra HTML5

Replaced the tmux+ttyd web terminal with actual kitty running through
Xpra's HTML5 streaming. Full GPU rendering, native kitty tabs/splits,
persistent sessions, and multi-client support.
This commit is contained in:
Mortdecai
2026-03-26 18:32:12 -04:00
parent 94eb19aa76
commit 8f3f2cae4c
5 changed files with 170 additions and 206 deletions
+88 -96
View File
@@ -1,137 +1,129 @@
# kitty-web # kitty-web
Mobile-first web terminal powered by [ttyd](https://github.com/tsl0922/ttyd) + [tmux](https://github.com/tmux/tmux). One persistent tmux session, multiple tabs, accessible from any browser. Designed for phones and tablets. Run the real [kitty terminal](https://sw.kovidgoyal.net/kitty/) in your browser. Mobile-friendly, GPU-accelerated, with full tab and split support.
Powered by [Xpra](https://xpra.org/) — serves kitty as an HTML5 application via its built-in web client. This is not a terminal emulator in JavaScript; it's the actual kitty running on your server, streamed to your browser.
## Features ## Features
- **Single persistent session** — one tmux session shared across all connected clients - **Real kitty** — GPU rendering, ligatures, image protocol, all of it
- **Mobile touch toolbar** — on-screen buttons for common shortcuts (new tab, ^C, ^D, Esc, arrows, split panes, etc.) - **Kitty tabs and splits** — native `ctrl+shift+t`, splits, layouts
- **Text selection mode** — tap `Sel` to enter selection mode, long-press to select and copy text, tap `Done` to resume typing - **Persistent session** — close the browser, reconnect later, everything is still there
- **Push notifications** — send browser notifications from any terminal command - **Multi-client** — multiple browsers can view/interact with the same session
- **PWA installable** — add to home screen for an app-like experience - **Mobile-friendly** — Xpra's HTML5 client handles touch input, keyboard, and scaling
- **Dark theme** — styled for dark-mode terminals with orange accents - **Push notifications** — optional notification API for long-running commands
## Architecture ## Architecture
``` ```
Browser -> Caddy (HTTPS + Auth) -> ttyd (port 7681) -> tmux session Browser -> Caddy (HTTPS + Auth) -> Xpra HTML5 (port 7681) -> kitty
-> notify-server (port 7682) -> /api/notifications
``` ```
- **ttyd** serves the terminal UI with a custom index page that loads `toolbar.js` Xpra runs kitty inside a virtual X display (Xvfb) and streams the rendered output to browsers via WebSocket. The HTML5 client handles input, clipboard, and display scaling.
- **toolbar.js** injects the mobile toolbar (shortcut buttons, selection mode) into the page at runtime
- **notify-server.py** provides a simple HTTP endpoint for push notifications
- **kitty-notify** is a CLI command to trigger notifications from scripts
## Quick Start ## Quick Start
### Prerequisites
- Debian/Ubuntu (tested on Debian 13 Trixie)
- kitty (`apt install kitty`)
- Xpra (`https://xpra.org/` — add their repo for latest version)
### Install
```bash ```bash
sudo ./install.sh # Add Xpra repo (Debian example)
curl -sL https://xpra.org/xpra.asc | sudo tee /usr/share/keyrings/xpra.asc
echo "deb [signed-by=/usr/share/keyrings/xpra.asc] https://xpra.org/ $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/xpra.list
sudo apt update && sudo apt install -y xpra kitty
# Create a service user (optional)
sudo useradd -m -s /bin/bash rdp
# Install systemd service
sudo cp systemd/kitty-web.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now kitty-web
``` ```
This installs ttyd, tmux, systemd services, and the notification system. The terminal will be available at `http://YOUR_IP:7681`. ### Manual Start
```bash
xpra start --bind-ws=0.0.0.0:7681 \
--start="kitty" \
--html=on \
--sharing=yes \
--no-daemon
```
Then open `http://YOUR_IP:7681` in a browser.
### Configuration ### Configuration
Set environment variables before running the installer: Edit `systemd/kitty-web.service` to customize:
| Variable | Default | Description | | Option | Description |
|----------|---------|-------------| |--------|-------------|
| `KITTY_USER` | `rdp` | System user that owns the tmux session | | `--bind-ws=HOST:PORT` | WebSocket listen address |
| `TTYD_PORT` | `7681` | Port for ttyd web terminal | | `--start="CMD"` | Application to launch (default: `kitty`) |
| `NOTIFY_PORT` | `7682` | Port for notification API | | `--sharing=yes` | Allow multiple clients to connect |
| `FONT_SIZE` | `18` | Terminal font size (optimized for mobile) | | `--readonly=no` | Allow keyboard/mouse input |
### Reverse Proxy ### Reverse Proxy (Caddy)
See `caddy-example.conf` for a complete Caddy v2 configuration with authentication. The setup supports OAuth2 Proxy, Authentik, or Authelia for access control. ```
terminal.example.com {
Key requirements for the reverse proxy: # Add your auth here (OAuth2 Proxy, Authentik, etc.)
- WebSocket support (ttyd uses WebSockets for terminal I/O) reverse_proxy YOUR_SERVER:7681
- Serve `/toolbar.js`, `/manifest.json`, `/icon-*.png` as static files }
- Proxy `/api/*` to the notification server (port 7682)
- Proxy everything else to ttyd (port 7681)
## Mobile Toolbar
The toolbar appears automatically on screens narrower than 900px. Buttons:
| Button | Action | tmux Key |
|--------|--------|----------|
| **+Tab** | New tab | `Ctrl-A c` |
| **Next** | Next tab | `Ctrl-A n` |
| **Prev** | Previous tab | `Ctrl-A p` |
| **^C** | Interrupt | `Ctrl-C` |
| **^D** | EOF / logout | `Ctrl-D` |
| **Clr** | Clear screen | `Ctrl-L` |
| **Esc** | Escape key | `Escape` |
| **Tab** | Tab completion | `Tab` |
| **Up/Down** | History navigation | Arrow keys |
| **Sel** | Toggle text selection mode | — |
| **Spl** | Split pane vertically | `Ctrl-A %` |
| **Pane** | Cycle between panes | `Ctrl-A o` |
| **Kill** | Kill current pane/tab | `Ctrl-A x` |
## Push Notifications
Send notifications from any terminal session:
```bash
# Direct message
kitty-notify "Build complete!"
# Pipe output
echo "Deploy finished" | kitty-notify
# Use in scripts
make build && kitty-notify "Build succeeded" || kitty-notify "Build FAILED"
``` ```
Notifications appear as browser push notifications on mobile. Tap the bell icon in the terminal UI to enable them. Notifications expire after 30 seconds. WebSocket support is required. Caddy handles this automatically. See `caddy-example.conf` for a full example with authentication options.
## tmux Keybindings ### Kitty Config
The included tmux config uses `Ctrl-A` as the prefix (easier on mobile than the default `Ctrl-B`): Place your kitty config at `~/.config/kitty/kitty.conf` for the service user. See `config/kitty.conf` for a dark-themed example.
| Key | Action | ## Optional: Push Notifications
|-----|--------|
| `Ctrl-A c` | New window/tab | The `notify-server.py` and `kitty-notify` command provide a simple browser notification system:
| `Ctrl-A n` / `Ctrl-A p` | Next / previous window |
| `Ctrl-A %` / `Ctrl-A "` | Split vertical / horizontal | ```bash
| `Ctrl-A o` | Cycle panes | # Install
| `Ctrl-A x` | Kill pane | sudo cp notify-server.py /opt/kitty-web/
| `Alt-1` through `Alt-5` | Jump to window 1-5 | sudo cp kitty-notify /usr/local/bin/
| `Alt-Left` / `Alt-Right` | Previous / next window | sudo cp systemd/kitty-notify.service /etc/systemd/system/
| `Alt-t` | New window | sudo systemctl enable --now kitty-notify
| Mouse scroll | Scroll through history |
# Usage
kitty-notify "Build complete!"
echo "done" | kitty-notify
```
Requires proxying `/api/*` to port 7682 — see `caddy-example.conf`.
## Files ## Files
``` ```
kitty-web/ kitty-web/
toolbar.js # Mobile toolbar (injected into ttyd page) README.md
notify-server.py # Push notification HTTP API LICENSE
kitty-notify # CLI notification command install.sh # Automated installer
manifest.json # PWA manifest caddy-example.conf # Reverse proxy config template
icon-192.png # PWA icon (192x192) notify-server.py # Push notification HTTP API (optional)
icon-512.png # PWA icon (512x512) kitty-notify # CLI notification command (optional)
install.sh # Installer script manifest.json # PWA manifest
caddy-example.conf # Reverse proxy configuration example icon-192.png # PWA icon
icon-512.png # PWA icon
config/ config/
tmux.conf # tmux configuration (dark theme, mobile-friendly) tmux.conf # tmux config (for optional tmux-inside-kitty usage)
kitty.conf # Kitty terminal config (dark theme)
systemd/ systemd/
ttyd-kitty.service # ttyd systemd unit kitty-web.service # Xpra + kitty systemd unit
kitty-notify.service # Notification API systemd unit kitty-notify.service # Notification API systemd unit
``` ```
## Requirements
- Linux (Debian/Ubuntu tested)
- tmux
- Python 3 (for notification server)
- [ttyd](https://github.com/tsl0922/ttyd) (installed automatically)
- Caddy, nginx, or another reverse proxy (for HTTPS and auth)
## License ## License
MIT MIT
+52
View File
@@ -0,0 +1,52 @@
# Sethian Terminal Theme
font_family JetBrains Mono
font_size 11.0
bold_font auto
italic_font auto
# Sethian colors - dark with orange accents
foreground #e0e0e0
background #0a0a0a
background_opacity 0.95
cursor #D35400
cursor_shape beam
selection_foreground #0a0a0a
selection_background #D35400
# Tab bar
active_tab_foreground #0a0a0a
active_tab_background #D35400
inactive_tab_foreground #999999
inactive_tab_background #1a1a1a
tab_bar_style powerline
# Normal colors
color0 #1a1a1a
color1 #cc3333
color2 #4e9a06
color3 #D35400
color4 #3465a4
color5 #75507b
color6 #06989a
color7 #d3d7cf
# Bright colors
color8 #555753
color9 #ef2929
color10 #8ae234
color11 #fce94f
color12 #729fcf
color13 #ad7fa8
color14 #34e2e2
color15 #eeeeec
# Window
window_padding_width 4
confirm_os_window_close 0
enable_audio_bell no
# URL handling
url_color #D35400
url_style curly
+30
View File
@@ -0,0 +1,30 @@
[Unit]
Description=Kitty terminal via Xpra HTML5 (kitty.sethpc.xyz)
After=network.target
[Service]
Type=simple
User=rdp
Group=rdp
WorkingDirectory=/home/rdp
Environment=XDG_RUNTIME_DIR=/run/user/1002
ExecStart=/usr/bin/xpra start \
--bind-ws=0.0.0.0:7681 \
--start="kitty" \
--html=on \
--no-notifications \
--no-pulseaudio \
--no-mdns \
--no-printing \
--sharing=yes \
--readonly=no \
--no-daemon
ExecStop=/usr/bin/xpra stop :0
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
-29
View File
@@ -1,29 +0,0 @@
[Unit]
Description=ttyd web terminal (kitty.sethpc.xyz)
After=network.target
[Service]
Type=simple
User=rdp
Group=rdp
ExecStartPre=/bin/bash -c "/usr/bin/tmux kill-session -t kitty 2>/dev/null; sleep 0.5; /usr/bin/tmux new-session -d -s kitty -x 120 -y 40"
ExecStart=/usr/local/bin/ttyd \
--port 7681 \
--interface 0.0.0.0 \
--index /opt/ttyd/index-with-toolbar.html \
--writable \
--check-origin \
--max-clients 5 \
--ping-interval 30 \
--client-option titleFixed=sethpc.xyz \
--client-option fontSize=18 \
--client-option fontFamily=monospace \
--client-option enableSixel=true \
/usr/bin/tmux attach-session -t kitty
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
-81
View File
@@ -1,81 +0,0 @@
(function(){
if(window._toolbar) return;
window._toolbar=true;
var css=document.createElement('style');
css.textContent=`
#mb{display:none;position:fixed;bottom:0;left:0;right:0;background:#111;
border-top:2px solid #D35400;padding:5px 4px;gap:4px;justify-content:center;
flex-wrap:wrap;z-index:99999}
#mb button{background:#222;color:#ccc;border:1px solid #444;border-radius:5px;
padding:10px 12px;font-size:14px;font-family:ui-monospace,monospace;
cursor:pointer;touch-action:manipulation;-webkit-tap-highlight-color:transparent;
min-width:42px;text-align:center;user-select:none}
#mb button:active{background:#D35400;color:#0a0a0a;border-color:#D35400}
#mb button.hi{border-color:#D35400;color:#D35400}
#mb button.on{background:#D35400;color:#0a0a0a;border-color:#D35400}
#mb .sep{width:1px;background:#333;margin:0 2px;align-self:stretch}
@media(max-width:900px){#mb{display:flex}}
body.selmode .xterm-screen{pointer-events:none!important;
user-select:text!important;-webkit-user-select:text!important}
`;
document.head.appendChild(css);
var bar=document.createElement('div');
bar.id='mb';
bar.innerHTML=
'<button class="hi" data-k="\\x01c">+Tab</button>'+
'<button data-k="\\x01n">Next</button>'+
'<button data-k="\\x01p">Prev</button>'+
'<div class="sep"></div>'+
'<button data-k="\\x03">^C</button>'+
'<button data-k="\\x04">^D</button>'+
'<button data-k="\\x0c">Clr</button>'+
'<div class="sep"></div>'+
'<button data-k="\\x1b">Esc</button>'+
'<button data-k="\\t">Tab</button>'+
'<button data-k="\\x1bOA">\u25B2</button>'+
'<button data-k="\\x1bOB">\u25BC</button>'+
'<div class="sep"></div>'+
'<button class="hi" id="selbtn" data-sel="1">Sel</button>'+
'<button data-k="\\x01%">Spl</button>'+
'<button data-k="\\x01o">Pane</button>'+
'<button data-k="\\x01x">Kill</button>';
document.body.appendChild(bar);
function send(k){
if(document.body.classList.contains('selmode')) toggleSel();
k=k.replace(/\\x([0-9a-f]{2})/gi,function(_,h){return String.fromCharCode(parseInt(h,16));});
k=k.replace(/\\t/g,'\t');
if(window.term){window.term.input(k);window.term.focus();}
}
function toggleSel(){
var b=document.getElementById('selbtn');
document.body.classList.toggle('selmode');
if(document.body.classList.contains('selmode')){
b.classList.add('on');b.textContent='Done';
} else {
b.classList.remove('on');b.textContent='Sel';
window.getSelection().removeAllRanges();
if(window.term) window.term.focus();
}
}
bar.addEventListener('click',function(e){
var btn=e.target.closest('button');
if(!btn) return;
if(btn.dataset.sel) return toggleSel();
if(btn.dataset.k) send(btn.dataset.k);
});
// Shrink terminal for toolbar on mobile
var obs=new MutationObserver(function(){
var el=document.querySelector('.xterm');
if(el && window.innerWidth<=900){
el.style.height='calc(100vh - 54px)';
if(window.term && window.term.fit) window.term.fit();
}
});
obs.observe(document.body,{childList:true,subtree:true});
})();