refactor: strip tmux/ttyd artifacts, kitty-web is now purely xpra+kitty
tmux web terminal moved to sethmux (git.sethpc.xyz/Seth/sethmux)
This commit is contained in:
@@ -1,129 +1,81 @@
|
|||||||
# kitty-web
|
# kitty-web
|
||||||
|
|
||||||
Run the real [kitty terminal](https://sw.kovidgoyal.net/kitty/) in your browser. Mobile-friendly, GPU-accelerated, with full tab and split support.
|
Run the real [kitty terminal](https://sw.kovidgoyal.net/kitty/) in your browser via [Xpra](https://xpra.org/) HTML5 streaming.
|
||||||
|
|
||||||
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.
|
This is not a JavaScript terminal emulator — it's the actual kitty with GPU rendering, image protocol, ligatures, and native tabs, streamed to your browser.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Real kitty** — GPU rendering, ligatures, image protocol, all of it
|
- **Real kitty** — GPU rendering, kitty image protocol, all features intact
|
||||||
- **Kitty tabs and splits** — native `ctrl+shift+t`, splits, layouts
|
- **Native kitty tabs/splits** — `Ctrl+Shift+T`, layouts, everything works
|
||||||
- **Persistent session** — close the browser, reconnect later, everything is still there
|
- **Persistent session** — close browser, reconnect later, session is still there
|
||||||
- **Multi-client** — multiple browsers can view/interact with the same session
|
- **Multi-client** — multiple browsers can connect simultaneously
|
||||||
- **Mobile-friendly** — Xpra's HTML5 client handles touch input, keyboard, and scaling
|
- **Mobile-friendly** — Xpra HTML5 client handles touch, scaling, on-screen keyboard
|
||||||
- **Push notifications** — optional notification API for long-running commands
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
Browser -> Caddy (HTTPS + Auth) -> Xpra HTML5 (port 7681) -> kitty
|
Browser -> Caddy (HTTPS + Auth) -> Xpra HTML5 (port 7681) -> kitty (Xvfb)
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
Xpra runs kitty in a virtual X display and streams the rendered pixels to browsers via WebSocket.
|
||||||
|
|
||||||
## Quick Start
|
## Setup
|
||||||
|
|
||||||
### Prerequisites
|
### 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
|
||||||
# Add Xpra repo (Debian example)
|
# Add Xpra repo
|
||||||
curl -sL https://xpra.org/xpra.asc | sudo tee /usr/share/keyrings/xpra.asc
|
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" | \
|
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 tee /etc/apt/sources.list.d/xpra.list
|
||||||
sudo apt update && sudo apt install -y xpra kitty
|
sudo apt update && sudo apt install -y xpra kitty
|
||||||
|
```
|
||||||
|
|
||||||
# Create a service user (optional)
|
### Install
|
||||||
sudo useradd -m -s /bin/bash rdp
|
|
||||||
|
|
||||||
# Install systemd service
|
```bash
|
||||||
sudo cp systemd/kitty-web.service /etc/systemd/system/
|
sudo cp systemd/kitty-web.service /etc/systemd/system/
|
||||||
|
sudo cp config/kitty.conf /home/YOUR_USER/.config/kitty/kitty.conf
|
||||||
|
sudo cp config/xpra-html5-settings.txt /usr/share/xpra/www/default-settings.txt
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
sudo systemctl enable --now kitty-web
|
sudo systemctl enable --now kitty-web
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manual Start
|
Open `http://YOUR_IP:7681` in a browser.
|
||||||
|
|
||||||
```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
|
|
||||||
|
|
||||||
Edit `systemd/kitty-web.service` to customize:
|
|
||||||
|
|
||||||
| Option | Description |
|
|
||||||
|--------|-------------|
|
|
||||||
| `--bind-ws=HOST:PORT` | WebSocket listen address |
|
|
||||||
| `--start="CMD"` | Application to launch (default: `kitty`) |
|
|
||||||
| `--sharing=yes` | Allow multiple clients to connect |
|
|
||||||
| `--readonly=no` | Allow keyboard/mouse input |
|
|
||||||
|
|
||||||
### Reverse Proxy (Caddy)
|
### Reverse Proxy (Caddy)
|
||||||
|
|
||||||
```
|
```
|
||||||
terminal.example.com {
|
kitty.example.com {
|
||||||
# Add your auth here (OAuth2 Proxy, Authentik, etc.)
|
# your auth here
|
||||||
reverse_proxy YOUR_SERVER:7681
|
reverse_proxy YOUR_SERVER:7681
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
WebSocket support is required. Caddy handles this automatically. See `caddy-example.conf` for a full example with authentication options.
|
## Mobile Tips
|
||||||
|
|
||||||
### Kitty Config
|
- **Floating menu** (circle in corner) — on-screen keyboard, fullscreen, clipboard, scaling
|
||||||
|
- **Pinch to zoom** works
|
||||||
Place your kitty config at `~/.config/kitty/kitty.conf` for the service user. See `config/kitty.conf` for a dark-themed example.
|
- **Kitty tab bar** at bottom — touch to switch tabs
|
||||||
|
- DPI set to 144 for readable text on mobile
|
||||||
## Optional: Push Notifications
|
|
||||||
|
|
||||||
The `notify-server.py` and `kitty-notify` command provide a simple browser notification system:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install
|
|
||||||
sudo cp notify-server.py /opt/kitty-web/
|
|
||||||
sudo cp kitty-notify /usr/local/bin/
|
|
||||||
sudo cp systemd/kitty-notify.service /etc/systemd/system/
|
|
||||||
sudo systemctl enable --now kitty-notify
|
|
||||||
|
|
||||||
# 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/
|
||||||
README.md
|
|
||||||
LICENSE
|
|
||||||
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
|
|
||||||
icon-192.png # PWA icon
|
|
||||||
icon-512.png # PWA icon
|
|
||||||
config/
|
config/
|
||||||
tmux.conf # tmux config (for optional tmux-inside-kitty usage)
|
kitty.conf # Kitty config (dark theme, 16pt, mobile-optimized)
|
||||||
kitty.conf # Kitty terminal config (dark theme)
|
xpra-html5-settings.txt # Xpra HTML5 client defaults
|
||||||
systemd/
|
systemd/
|
||||||
kitty-web.service # Xpra + kitty systemd unit
|
kitty-web.service # Xpra + kitty systemd unit
|
||||||
kitty-notify.service # Notification API systemd unit
|
caddy-example.conf # Reverse proxy template
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [sethmux](https://git.sethpc.xyz/Seth/sethmux) — lightweight web terminal (ttyd + tmux) at `mux.sethpc.xyz`
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
# Sethian tmux config
|
|
||||||
|
|
||||||
# Remap prefix to Ctrl-a (easier on mobile)
|
|
||||||
unbind C-b
|
|
||||||
set -g prefix C-a
|
|
||||||
bind C-a send-prefix
|
|
||||||
|
|
||||||
# Easy tab management
|
|
||||||
bind -n M-t new-window
|
|
||||||
bind -n M-w kill-window
|
|
||||||
bind -n M-1 select-window -t 0
|
|
||||||
bind -n M-2 select-window -t 1
|
|
||||||
bind -n M-3 select-window -t 2
|
|
||||||
bind -n M-4 select-window -t 3
|
|
||||||
bind -n M-5 select-window -t 4
|
|
||||||
bind -n M-Left previous-window
|
|
||||||
bind -n M-Right next-window
|
|
||||||
|
|
||||||
# Mouse support (critical for mobile/touch)
|
|
||||||
set -g mouse on
|
|
||||||
|
|
||||||
# Scrollback
|
|
||||||
set -g history-limit 50000
|
|
||||||
|
|
||||||
# Start numbering at 1
|
|
||||||
set -g base-index 1
|
|
||||||
setw -g pane-base-index 1
|
|
||||||
|
|
||||||
# Renumber windows on close
|
|
||||||
set -g renumber-windows on
|
|
||||||
|
|
||||||
# Status bar - Sethian dark + orange
|
|
||||||
set -g status-style "bg=#1a1a1a,fg=#e0e0e0"
|
|
||||||
set -g status-left "#[bg=#D35400,fg=#0a0a0a,bold] #S #[bg=#1a1a1a] "
|
|
||||||
set -g status-right "#[fg=#D35400]%H:%M #[fg=#666666]| #[fg=#e0e0e0]%b %d"
|
|
||||||
set -g status-left-length 20
|
|
||||||
set -g status-right-length 30
|
|
||||||
|
|
||||||
# Window status
|
|
||||||
setw -g window-status-format " #[fg=#888888]#I:#W "
|
|
||||||
setw -g window-status-current-format "#[bg=#D35400,fg=#0a0a0a,bold] #I:#W "
|
|
||||||
setw -g window-status-separator ""
|
|
||||||
|
|
||||||
# Pane borders
|
|
||||||
set -g pane-border-style "fg=#333333"
|
|
||||||
set -g pane-active-border-style "fg=#D35400"
|
|
||||||
|
|
||||||
# Terminal settings
|
|
||||||
set -g default-terminal "tmux-256color"
|
|
||||||
set -ga terminal-overrides ",xterm-256color:Tc"
|
|
||||||
|
|
||||||
# Reduce escape delay
|
|
||||||
set -sg escape-time 10
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
-92
@@ -1,92 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# kitty-web installer
|
|
||||||
# Mobile-first web terminal with tmux, ttyd, and push notifications
|
|
||||||
set -e
|
|
||||||
|
|
||||||
TTYD_VERSION="1.7.7"
|
|
||||||
SERVICE_USER="${KITTY_USER:-rdp}"
|
|
||||||
TTYD_PORT="${TTYD_PORT:-7681}"
|
|
||||||
NOTIFY_PORT="${NOTIFY_PORT:-7682}"
|
|
||||||
FONT_SIZE="${FONT_SIZE:-18}"
|
|
||||||
|
|
||||||
echo "=== kitty-web installer ==="
|
|
||||||
echo "Service user: $SERVICE_USER"
|
|
||||||
echo "ttyd port: $TTYD_PORT"
|
|
||||||
echo "Notify port: $NOTIFY_PORT"
|
|
||||||
|
|
||||||
# Check root
|
|
||||||
if [ "$EUID" -ne 0 ]; then
|
|
||||||
echo "Run as root"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
echo "[1/6] Installing dependencies..."
|
|
||||||
apt install -y tmux 2>/dev/null || true
|
|
||||||
|
|
||||||
# Install ttyd
|
|
||||||
if ! command -v ttyd &>/dev/null; then
|
|
||||||
echo "[2/6] Installing ttyd $TTYD_VERSION..."
|
|
||||||
curl -sL "https://github.com/tsl0922/ttyd/releases/latest/download/ttyd.x86_64" -o /usr/local/bin/ttyd
|
|
||||||
chmod +x /usr/local/bin/ttyd
|
|
||||||
else
|
|
||||||
echo "[2/6] ttyd already installed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create user if needed
|
|
||||||
if ! id "$SERVICE_USER" &>/dev/null; then
|
|
||||||
echo "[3/6] Creating user $SERVICE_USER..."
|
|
||||||
useradd -m -s /bin/bash "$SERVICE_USER"
|
|
||||||
else
|
|
||||||
echo "[3/6] User $SERVICE_USER exists"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Deploy files
|
|
||||||
echo "[4/6] Deploying files..."
|
|
||||||
mkdir -p /opt/kitty-web/static
|
|
||||||
|
|
||||||
# Build custom index (inject toolbar into ttyd's default page)
|
|
||||||
TMPINDEX=$(mktemp)
|
|
||||||
ttyd --port 0 /bin/true &
|
|
||||||
TTYD_PID=$!
|
|
||||||
sleep 1
|
|
||||||
# Can't easily grab default page without a running instance, so we'll
|
|
||||||
# add toolbar.js loading to the page at runtime via the notify server
|
|
||||||
kill $TTYD_PID 2>/dev/null || true
|
|
||||||
|
|
||||||
cp "$(dirname "$0")/toolbar.js" /opt/kitty-web/static/toolbar.js
|
|
||||||
cp "$(dirname "$0")/manifest.json" /opt/kitty-web/static/manifest.json
|
|
||||||
cp "$(dirname "$0")/icon-192.png" /opt/kitty-web/static/icon-192.png 2>/dev/null || true
|
|
||||||
cp "$(dirname "$0")/icon-512.png" /opt/kitty-web/static/icon-512.png 2>/dev/null || true
|
|
||||||
cp "$(dirname "$0")/notify-server.py" /opt/kitty-web/notify-server.py
|
|
||||||
chmod +x /opt/kitty-web/notify-server.py
|
|
||||||
|
|
||||||
# Install kitty-notify command
|
|
||||||
cp "$(dirname "$0")/kitty-notify" /usr/local/bin/kitty-notify
|
|
||||||
chmod +x /usr/local/bin/kitty-notify
|
|
||||||
|
|
||||||
# Install tmux config
|
|
||||||
sudo -u "$SERVICE_USER" cp "$(dirname "$0")/config/tmux.conf" "$(eval echo ~$SERVICE_USER)/.tmux.conf" 2>/dev/null || true
|
|
||||||
|
|
||||||
# Install systemd services
|
|
||||||
echo "[5/6] Installing services..."
|
|
||||||
sed "s/User=rdp/User=$SERVICE_USER/g; s/Group=rdp/Group=$SERVICE_USER/g; s/--port 7681/--port $TTYD_PORT/g; s/fontSize=18/fontSize=$FONT_SIZE/g" \
|
|
||||||
"$(dirname "$0")/systemd/ttyd-kitty.service" > /etc/systemd/system/ttyd-kitty.service
|
|
||||||
|
|
||||||
sed "s/User=rdp/User=$SERVICE_USER/g" \
|
|
||||||
"$(dirname "$0")/systemd/kitty-notify.service" > /etc/systemd/system/kitty-notify.service
|
|
||||||
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl enable --now ttyd-kitty kitty-notify
|
|
||||||
|
|
||||||
echo "[6/6] Verifying..."
|
|
||||||
sleep 2
|
|
||||||
systemctl is-active ttyd-kitty && echo " ttyd: OK (port $TTYD_PORT)"
|
|
||||||
systemctl is-active kitty-notify && echo " notify: OK (port $NOTIFY_PORT)"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== kitty-web is running ==="
|
|
||||||
echo "Direct access: http://$(hostname -I | awk '{print $1}'):$TTYD_PORT"
|
|
||||||
echo ""
|
|
||||||
echo "For reverse proxy setup, see README.md"
|
|
||||||
echo "Send notifications: kitty-notify 'Hello from the terminal!'"
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Send a push notification to kitty.sethpc.xyz
|
|
||||||
# Usage: kitty-notify "Build complete!" or echo "done" | kitty-notify
|
|
||||||
if [ -n "$1" ]; then
|
|
||||||
echo "$*" > /tmp/kitty-notify
|
|
||||||
else
|
|
||||||
cat > /tmp/kitty-notify
|
|
||||||
fi
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "sethpc terminal",
|
|
||||||
"short_name": "kitty",
|
|
||||||
"start_url": "/",
|
|
||||||
"display": "standalone",
|
|
||||||
"background_color": "#0a0a0a",
|
|
||||||
"theme_color": "#D35400",
|
|
||||||
"icons": [
|
|
||||||
{"src": "/icon-192.png", "sizes": "192x192", "type": "image/png"},
|
|
||||||
{"src": "/icon-512.png", "sizes": "512x512", "type": "image/png"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Tiny HTTP server for terminal notifications. Serves /api/notifications."""
|
|
||||||
import http.server
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
|
|
||||||
NOTIFY_FILE = "/tmp/kitty-notify"
|
|
||||||
PORT = 7682
|
|
||||||
|
|
||||||
class Handler(http.server.BaseHTTPRequestHandler):
|
|
||||||
def do_GET(self):
|
|
||||||
if self.path == "/api/notifications":
|
|
||||||
msg = ""
|
|
||||||
if os.path.exists(NOTIFY_FILE):
|
|
||||||
try:
|
|
||||||
mtime = os.path.getmtime(NOTIFY_FILE)
|
|
||||||
if time.time() - mtime < 30: # only show notifications < 30s old
|
|
||||||
with open(NOTIFY_FILE) as f:
|
|
||||||
msg = f.read().strip()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
self.send_response(200)
|
|
||||||
self.send_header("Content-Type", "application/json")
|
|
||||||
self.send_header("Access-Control-Allow-Origin", "*")
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(json.dumps({"message": msg}).encode())
|
|
||||||
else:
|
|
||||||
self.send_response(404)
|
|
||||||
self.end_headers()
|
|
||||||
|
|
||||||
def log_message(self, format, *args):
|
|
||||||
pass # quiet
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
server = http.server.HTTPServer(("0.0.0.0", PORT), Handler)
|
|
||||||
server.serve_forever()
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Kitty terminal notification API
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=rdp
|
|
||||||
ExecStart=/usr/bin/python3 /opt/ttyd/notify-server.py
|
|
||||||
Restart=always
|
|
||||||
RestartSec=3
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
Reference in New Issue
Block a user