-- after/ftplugin/html.lua  (or just :source % once)
local port = 8085
local root = vim.loop.cwd()
local job_id = nil

vim.api.nvim_create_user_command("Serve", function()
  -- kill previous python instance
  if job_id then
    vim.fn.jobstop(job_id)
  end

  -- one-liner HTTP server + websocket broadcast
  local script = ([[
import http.server, os, subprocess, threading, time, mimetypes, json
from http.server import HTTPServer, SimpleHTTPRequestHandler
import websockets, asyncio, pathlib

ROOT    = %q
PORT    = %d
clients = set()

class Handler(SimpleHTTPRequestHandler):
    def log_message(self, fmt, *args): pass   # shut up
    def do_GET(self):
        if self.path == '/':
            self.path = '/index.html'
        # inject tiny reload script into every HTML
        if self.path.endswith('.html'):
            ctype = 'text/html; charset=utf-8'
            with open(os.path.join(ROOT, self.path.lstrip('/')), 'rb') as f:
                content = f.read().decode()
            inject = '<script>let ws=new WebSocket("ws://localhost:%d/ws");ws.onmessage=()=>location.reload()</script>'
            content = content.replace('</body>', inject .. '</body>')
            self.send_response(200)
            self.send_header('Content-type', ctype)
            self.end_headers()
            self.wfile.write(content.encode())
        else:
            super().do_GET()

async def ws_handler(websocket):
    clients.add(websocket)
    try:
        await websocket.wait_closed()
    finally:
        clients.remove(websocket)

async def websocket_main():
    await websockets.serve(ws_handler, "localhost", %d, path="/ws")

def broadcast_reload():
    for c in clients:
        asyncio.run(c.send("reload"))
    clients.clear()

def reload_loop():
    while True:
        time.sleep(0.05)
        try:
            subprocess.check_call(["inotifywait", "-e", "close_write", "-t", "0", ROOT],
                                  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
            broadcast_reload()
        except: pass   # inotify not installed → noop (manual F5 still works)

def main():
    threading.Thread(target=reload_loop, daemon=True).start()
    asyncio.set_event_loop(asyncio.new_event_loop())
    threading.Thread(target=lambda: asyncio.run(websocket_main()), daemon=True).start()
    HTTPServer(("", PORT), Handler).serve_forever()

if __name__ == '__main__':
    main()
]]):format(root, port, port, port)

  job_id = vim.fn.jobstart({ "python", "-c", script }, { stdout_buffered = true })
  vim.fn.jobstart({ "sleep", "0.2" }, {
    on_exit = function()
      vim.ui.open(("http://localhost:%d"):format(port))
    end,
  })
  vim.notify("Serving → http://localhost:" .. port, vim.log.levels.INFO)
end, {})

vim.api.nvim_create_user_command("ServeStop", function()
  if job_id then
    vim.fn.jobstop(job_id)
    job_id = nil
  end
end, {})
