From 5f1beb4d4d271ccec53aa219d6f8cadac21870f3 Mon Sep 17 00:00:00 2001 From: Mortdecai Date: Thu, 26 Mar 2026 18:59:37 -0400 Subject: [PATCH] feat: initial release - mobile-first web terminal with tmux touch toolbar and push notifications --- .gitignore | 6 ++ LICENSE | 21 ++++++ README.md | 113 +++++++++++++++++++++++++++++++++ config/tmux.conf | 53 ++++++++++++++++ notify-server.py | 37 +++++++++++ sethmux-notify | 8 +++ static/icon-192.png | Bin 0 -> 3809 bytes static/icon-512.png | Bin 0 -> 10782 bytes static/index.html | 3 + static/manifest.json | 12 ++++ static/toolbar.js | 81 +++++++++++++++++++++++ systemd/sethmux-notify.service | 13 ++++ systemd/sethmux.service | 29 +++++++++ 13 files changed, 376 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 config/tmux.conf create mode 100755 notify-server.py create mode 100755 sethmux-notify create mode 100644 static/icon-192.png create mode 100644 static/icon-512.png create mode 100644 static/index.html create mode 100644 static/manifest.json create mode 100644 static/toolbar.js create mode 100644 systemd/sethmux-notify.service create mode 100644 systemd/sethmux.service diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c56c6cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.env +.env* +*.pem +*.key +__pycache__/ +*.pyc diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e16fdbc --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..49bb47a --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +# sethmux + +Mobile-first web terminal powered by [ttyd](https://github.com/tsl0922/ttyd) + [tmux](https://github.com/tmux/tmux). One persistent session, multiple tabs, accessible from any browser. + +## Features + +- **Single persistent tmux session** — shared across all connected clients +- **Mobile touch toolbar** — on-screen buttons for tabs, signals, navigation, splits, and text selection +- **Text selection mode** — tap `Sel` to select and copy text on touch devices +- **Push notifications** — `sethmux-notify "Build done!"` sends browser notifications +- **PWA installable** — add to home screen for app-like experience +- **Dark theme** — Sethian dark + orange (#D35400) accent + +## Architecture + +``` +Browser -> Caddy (HTTPS + Auth) -> ttyd (port 7683) -> tmux session "sethmux" + -> notify-server (port 7684) -> /api/notifications +``` + +## Quick Start + +```bash +# Dependencies +apt install -y tmux +curl -sL https://github.com/tsl0922/ttyd/releases/latest/download/ttyd.x86_64 -o /usr/local/bin/ttyd +chmod +x /usr/local/bin/ttyd + +# Deploy +sudo mkdir -p /opt/sethmux +sudo cp static/* /opt/sethmux/ +sudo cp notify-server.py /opt/sethmux/ +sudo cp sethmux-notify /usr/local/bin/ +sudo cp config/tmux.conf ~/.tmux.conf +sudo cp systemd/*.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable --now sethmux sethmux-notify +``` + +Open `http://YOUR_IP:7683` in a browser. + +## Mobile Toolbar + +Appears on screens < 900px. Buttons: + +| Button | Action | tmux Key | +|--------|--------|----------| +| **+Tab** | New tab | `Ctrl-A c` | +| **Next/Prev** | Switch tabs | `Ctrl-A n/p` | +| **^C / ^D / Clr** | Interrupt / EOF / Clear | | +| **Esc / Tab / Up / Down** | Navigation | | +| **Sel** | Text selection mode | | +| **Spl** | Split vertical | `Ctrl-A %` | +| **Pane** | Cycle panes | `Ctrl-A o` | +| **Kill** | Kill pane/tab | `Ctrl-A x` | + +## Push Notifications + +```bash +sethmux-notify "Deploy complete!" +echo "done" | sethmux-notify +make build && sethmux-notify "OK" || sethmux-notify "FAIL" +``` + +## tmux Keybindings + +Prefix: `Ctrl-A` (not the default Ctrl-B — easier on mobile). + +| Key | Action | +|-----|--------| +| `Ctrl-A c` | New window | +| `Ctrl-A n / p` | Next / previous | +| `Ctrl-A % / "` | Split vertical / horizontal | +| `Ctrl-A o` | Cycle panes | +| `Ctrl-A x` | Kill pane | +| `Alt-1` to `Alt-5` | Jump to window | +| Mouse scroll | History | + +## Reverse Proxy (Caddy) + +``` +mux.example.com { + # your auth here + handle /toolbar.js { root * /opt/sethmux; file_server } + handle /manifest.json { root * /opt/sethmux; file_server } + handle /icon-*.png { root * /opt/sethmux; file_server } + handle /api/* { uri strip_prefix /api; reverse_proxy localhost:7684 } + handle { reverse_proxy localhost:7683 } +} +``` + +## Files + +``` +sethmux/ + static/ + index.html # Custom ttyd page with toolbar injection + toolbar.js # Mobile touch toolbar + manifest.json # PWA manifest + icon-192.png # PWA icon + icon-512.png # PWA icon + config/ + tmux.conf # Sethian-themed tmux config + systemd/ + sethmux.service # ttyd + tmux systemd unit + sethmux-notify.service # Notification API unit + notify-server.py # Push notification HTTP API + sethmux-notify # CLI notification command +``` + +## License + +MIT diff --git a/config/tmux.conf b/config/tmux.conf new file mode 100644 index 0000000..9177d2e --- /dev/null +++ b/config/tmux.conf @@ -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 diff --git a/notify-server.py b/notify-server.py new file mode 100755 index 0000000..a4a7292 --- /dev/null +++ b/notify-server.py @@ -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() diff --git a/sethmux-notify b/sethmux-notify new file mode 100755 index 0000000..39dfca4 --- /dev/null +++ b/sethmux-notify @@ -0,0 +1,8 @@ +#!/bin/bash +# Send a push notification to kitty.sethpc.xyz +# Usage: sethmux-notify "Build complete!" or echo "done" | sethmux-notify +if [ -n "$1" ]; then + echo "$*" > /tmp/sethmux-notify +else + cat > /tmp/sethmux-notify +fi diff --git a/static/icon-192.png b/static/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..7cb08756db3ede479f7c9490b4233a2b434f5a2d GIT binary patch literal 3809 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 Kex;uCqkjQ{s_|U_ literal 0 HcmV?d00001 diff --git a/static/icon-512.png b/static/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..ebffc7ee1b537343580a1f098a9c76e927d55b52 GIT binary patch literal 10782 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 J{-rsethmux \ No newline at end of file diff --git a/static/manifest.json b/static/manifest.json new file mode 100644 index 0000000..3509b06 --- /dev/null +++ b/static/manifest.json @@ -0,0 +1,12 @@ +{ + "name": "sethmux", + "short_name": "sethmux", + "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"} + ] +} diff --git a/static/toolbar.js b/static/toolbar.js new file mode 100644 index 0000000..ed90a7f --- /dev/null +++ b/static/toolbar.js @@ -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= + ''+ + ''+ + ''+ + '
'+ + ''+ + ''+ + ''+ + '
'+ + ''+ + ''+ + ''+ + ''+ + '
'+ + ''+ + ''+ + ''+ + ''; + 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}); +})(); diff --git a/systemd/sethmux-notify.service b/systemd/sethmux-notify.service new file mode 100644 index 0000000..7cfb43d --- /dev/null +++ b/systemd/sethmux-notify.service @@ -0,0 +1,13 @@ +[Unit] +Description=sethmux notification API +After=network.target + +[Service] +Type=simple +User=rdp +ExecStart=/usr/bin/python3 /opt/sethmux/notify-server.py +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/systemd/sethmux.service b/systemd/sethmux.service new file mode 100644 index 0000000..254bcb4 --- /dev/null +++ b/systemd/sethmux.service @@ -0,0 +1,29 @@ +[Unit] +Description=sethmux - web terminal (mux.sethpc.xyz) +After=network.target + +[Service] +Type=simple +User=rdp +Group=rdp + +ExecStartPre=/bin/bash -c "/usr/bin/tmux kill-session -t sethmux 2>/dev/null; sleep 0.5; /usr/bin/tmux new-session -d -s sethmux -x 120 -y 40" +ExecStart=/usr/local/bin/ttyd \ + --port 7683 \ + --interface 0.0.0.0 \ + --index /opt/sethmux/index.html \ + --writable \ + --check-origin \ + --max-clients 5 \ + --ping-interval 30 \ + --client-option titleFixed=sethmux \ + --client-option fontSize=18 \ + --client-option fontFamily=monospace \ + --client-option enableSixel=true \ + /usr/bin/tmux attach-session -t sethmux + +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target