ssh / wspty
anlaki's picture
Upload wspty
d98022d verified
Raw
History Blame Contribute Delete
2.54 kB
#!/usr/bin/env python3
"""WebSocket PTY client — like SSH but over WebSocket.
Puts your terminal in raw mode so TUIs (vim, nano, htop) work.
"""
import asyncio
import json
import os
import signal
import sys
import termios
import threading
import tty
import websockets
async def main():
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} wss://host[:port]", file=sys.stderr)
sys.exit(1)
url = sys.argv[1]
if not url.startswith(("ws://", "wss://")):
url = f"wss://{url}"
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
tty.setraw(fd)
cols, rows = os.get_terminal_size()
stdin_queue: asyncio.Queue = asyncio.Queue()
def stdin_reader():
try:
while True:
data = os.read(fd, 65536)
if not data:
break
stdin_queue.put_nowait(data)
except Exception:
pass
threading.Thread(target=stdin_reader, daemon=True).start()
def on_resize(signum, frame):
nonlocal cols, rows
try:
nc, nr = os.get_terminal_size()
if nc != cols or nr != rows:
cols, rows = nc, nr
except Exception:
pass
signal.signal(signal.SIGWINCH, on_resize)
try:
async with websockets.connect(url, max_size=2**20, ping_interval=None) as ws:
# Send initial resize
await ws.send(json.dumps({"rows": rows, "cols": cols}))
async def sender():
last_c, last_r = cols, rows
while True:
if cols != last_c or rows != last_r:
await ws.send(json.dumps({"rows": rows, "cols": cols}))
last_c, last_r = cols, rows
try:
data = await asyncio.wait_for(stdin_queue.get(), timeout=0.1)
await ws.send(data) # bytes → binary frame
except asyncio.TimeoutError:
continue
async def receiver():
async for data in ws:
if isinstance(data, str):
continue # skip text frames (resize acks etc.)
sys.stdout.buffer.write(data)
sys.stdout.buffer.flush()
await asyncio.gather(sender(), receiver())
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
print() # newline after raw mode
if __name__ == "__main__":
asyncio.run(main())