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:
@@ -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
|
||||||
|
caddy-example.conf # Reverse proxy config template
|
||||||
|
notify-server.py # Push notification HTTP API (optional)
|
||||||
|
kitty-notify # CLI notification command (optional)
|
||||||
manifest.json # PWA manifest
|
manifest.json # PWA manifest
|
||||||
icon-192.png # PWA icon (192x192)
|
icon-192.png # PWA icon
|
||||||
icon-512.png # PWA icon (512x512)
|
icon-512.png # PWA icon
|
||||||
install.sh # Installer script
|
|
||||||
caddy-example.conf # Reverse proxy configuration example
|
|
||||||
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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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});
|
|
||||||
})();
|
|
||||||
Reference in New Issue
Block a user