feat: initial release - mobile-first web terminal with tmux, toolbar, and push notifications
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
*.env
|
||||||
|
.env*
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Seth Freiberg
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Single persistent session** — one tmux session shared across all connected clients
|
||||||
|
- **Mobile touch toolbar** — on-screen buttons for common shortcuts (new tab, ^C, ^D, Esc, arrows, split panes, etc.)
|
||||||
|
- **Text selection mode** — tap `Sel` to enter selection mode, long-press to select and copy text, tap `Done` to resume typing
|
||||||
|
- **Push notifications** — send browser notifications from any terminal command
|
||||||
|
- **PWA installable** — add to home screen for an app-like experience
|
||||||
|
- **Dark theme** — styled for dark-mode terminals with orange accents
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Browser -> Caddy (HTTPS + Auth) -> ttyd (port 7681) -> tmux session
|
||||||
|
-> notify-server (port 7682) -> /api/notifications
|
||||||
|
```
|
||||||
|
|
||||||
|
- **ttyd** serves the terminal UI with a custom index page that loads `toolbar.js`
|
||||||
|
- **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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This installs ttyd, tmux, systemd services, and the notification system. The terminal will be available at `http://YOUR_IP:7681`.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Set environment variables before running the installer:
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `KITTY_USER` | `rdp` | System user that owns the tmux session |
|
||||||
|
| `TTYD_PORT` | `7681` | Port for ttyd web terminal |
|
||||||
|
| `NOTIFY_PORT` | `7682` | Port for notification API |
|
||||||
|
| `FONT_SIZE` | `18` | Terminal font size (optimized for mobile) |
|
||||||
|
|
||||||
|
### Reverse Proxy
|
||||||
|
|
||||||
|
See `caddy-example.conf` for a complete Caddy v2 configuration with authentication. The setup supports OAuth2 Proxy, Authentik, or Authelia for access control.
|
||||||
|
|
||||||
|
Key requirements for the reverse proxy:
|
||||||
|
- WebSocket support (ttyd uses WebSockets for terminal I/O)
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
## tmux Keybindings
|
||||||
|
|
||||||
|
The included tmux config uses `Ctrl-A` as the prefix (easier on mobile than the default `Ctrl-B`):
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|-----|--------|
|
||||||
|
| `Ctrl-A c` | New window/tab |
|
||||||
|
| `Ctrl-A n` / `Ctrl-A p` | Next / previous window |
|
||||||
|
| `Ctrl-A %` / `Ctrl-A "` | Split vertical / horizontal |
|
||||||
|
| `Ctrl-A o` | Cycle panes |
|
||||||
|
| `Ctrl-A x` | Kill pane |
|
||||||
|
| `Alt-1` through `Alt-5` | Jump to window 1-5 |
|
||||||
|
| `Alt-Left` / `Alt-Right` | Previous / next window |
|
||||||
|
| `Alt-t` | New window |
|
||||||
|
| Mouse scroll | Scroll through history |
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
```
|
||||||
|
kitty-web/
|
||||||
|
toolbar.js # Mobile toolbar (injected into ttyd page)
|
||||||
|
notify-server.py # Push notification HTTP API
|
||||||
|
kitty-notify # CLI notification command
|
||||||
|
manifest.json # PWA manifest
|
||||||
|
icon-192.png # PWA icon (192x192)
|
||||||
|
icon-512.png # PWA icon (512x512)
|
||||||
|
install.sh # Installer script
|
||||||
|
caddy-example.conf # Reverse proxy configuration example
|
||||||
|
config/
|
||||||
|
tmux.conf # tmux configuration (dark theme, mobile-friendly)
|
||||||
|
systemd/
|
||||||
|
ttyd-kitty.service # ttyd 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
|
||||||
|
|
||||||
|
MIT
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Example Caddy reverse proxy config for kitty-web
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# - Caddy v2 with HTTPS
|
||||||
|
# - OAuth2 Proxy, Authentik, or Authelia for authentication (recommended)
|
||||||
|
# - ttyd running on TTYD_HOST:7681
|
||||||
|
# - notify server running on TTYD_HOST:7682
|
||||||
|
#
|
||||||
|
# Replace TTYD_HOST with the IP/hostname of the machine running kitty-web.
|
||||||
|
# Replace the auth snippet with your own authentication setup.
|
||||||
|
|
||||||
|
# --- Authentication snippet (pick one) ---
|
||||||
|
|
||||||
|
# Option A: OAuth2 Proxy (Google Auth)
|
||||||
|
# (google_auth) {
|
||||||
|
# forward_auth OAUTH2_PROXY_HOST:4180 {
|
||||||
|
# uri /oauth2/auth
|
||||||
|
# header_up Host auth.example.com
|
||||||
|
# copy_headers X-Auth-Request-User X-Auth-Request-Email
|
||||||
|
# handle_response @unauthorized {
|
||||||
|
# redir https://auth.example.com/oauth2/start?rd={scheme}://{host}{uri}
|
||||||
|
# }
|
||||||
|
# @unauthorized status 401
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
|
# Option B: Authentik
|
||||||
|
# (authentik) {
|
||||||
|
# forward_auth AUTHENTIK_HOST:9000 {
|
||||||
|
# uri /outpost.goauthentik.io/auth/caddy
|
||||||
|
# copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
|
# --- Site block ---
|
||||||
|
|
||||||
|
# terminal.example.com {
|
||||||
|
# import google_auth
|
||||||
|
#
|
||||||
|
# # Undo any cached permanent redirects from old configs
|
||||||
|
# @oldttyd path /ttyd /ttyd/
|
||||||
|
# redir @oldttyd / temporary
|
||||||
|
#
|
||||||
|
# # Static assets (toolbar, PWA manifest, icons)
|
||||||
|
# handle /toolbar.js {
|
||||||
|
# root * /opt/kitty-web/static
|
||||||
|
# file_server
|
||||||
|
# }
|
||||||
|
# handle /manifest.json {
|
||||||
|
# root * /opt/kitty-web/static
|
||||||
|
# file_server
|
||||||
|
# }
|
||||||
|
# handle /icon-*.png {
|
||||||
|
# root * /opt/kitty-web/static
|
||||||
|
# file_server
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# # Notification API
|
||||||
|
# handle /api/* {
|
||||||
|
# uri strip_prefix /api
|
||||||
|
# reverse_proxy TTYD_HOST:7682
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# # Terminal (catch-all)
|
||||||
|
# handle {
|
||||||
|
# reverse_proxy TTYD_HOST:7681
|
||||||
|
# }
|
||||||
|
# }
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# 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.
|
After Width: | Height: | Size: 3.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Executable
+92
@@ -0,0 +1,92 @@
|
|||||||
|
#!/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!'"
|
||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/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
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"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"}
|
||||||
|
]
|
||||||
|
}
|
||||||
Executable
+37
@@ -0,0 +1,37 @@
|
|||||||
|
#!/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()
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[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
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
[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
@@ -0,0 +1,81 @@
|
|||||||
|
(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