| |
| """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: |
| |
| 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) |
| except asyncio.TimeoutError: |
| continue |
|
|
| async def receiver(): |
| async for data in ws: |
| if isinstance(data, str): |
| continue |
| sys.stdout.buffer.write(data) |
| sys.stdout.buffer.flush() |
|
|
| await asyncio.gather(sender(), receiver()) |
| finally: |
| termios.tcsetattr(fd, termios.TCSADRAIN, old) |
| print() |
|
|
|
|
| if __name__ == "__main__": |
| asyncio.run(main()) |
|
|