#!/usr/bin/env python3 import http.server import socketserver import threading import os import time import subprocess from pathlib import Path from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler import urllib.parse # ----------------------------- # Configuration # ----------------------------- ROOT = Path(os.getcwd()) # root wiki folder LAST_UPDATE = time.time() AWK_SCRIPT = ROOT / "gem2html.awk" # path to your AWK converter VALID_SUFFIXES = (".md",) # valid Gemtext files (all your files are .md) PORT = 8001 # ----------------------------- # File watcher # ----------------------------- class ChangeHandler(FileSystemEventHandler): def on_any_event(self, event): global LAST_UPDATE LAST_UPDATE = time.time() def start_watcher(): observer = Observer() observer.schedule(ChangeHandler(), str(ROOT), recursive=True) observer.start() # ----------------------------- # File tree sidebar # ----------------------------- def generate_file_tree(): html = ["") return "\n".join(html) # ----------------------------- # AWK Gemtext -> HTML conversion # ----------------------------- def gemtext_to_html(file_path: Path) -> str: try: result = subprocess.run( ["awk", "-f", str(AWK_SCRIPT), str(file_path)], capture_output=True, text=True, check=True ) return result.stdout except subprocess.CalledProcessError as e: return f"
Error running AWK:\n{e.stderr}
" # ----------------------------- # Wrap HTML with sidebar + auto-refresh # ----------------------------- def wrap_html(body: str, file_tree: str) -> str: return f""" GemLive Preview
{body}
""" # ----------------------------- # HTTP handler # ----------------------------- class Handler(http.server.SimpleHTTPRequestHandler): def send_event(self): self.send_response(200) self.send_header("Content-Type", "text/event-stream") self.send_header("Cache-Control", "no-cache") self.end_headers() last = LAST_UPDATE while True: if LAST_UPDATE != last: self.wfile.write(b"data: reload\n\n") self.wfile.flush() return time.sleep(0.1) def do_GET(self): if self.path == "/__events": return self.send_event() # URL-decode and strip leading slash rel_path = urllib.parse.unquote(self.path.lstrip("/")) if not rel_path: rel_path = "index.md" path = ROOT / rel_path # if path is a directory, serve its index.md if path.is_dir(): path = path / "index.md" if path.exists() and path.suffix in VALID_SUFFIXES: html_body = gemtext_to_html(path) html = wrap_html(html_body, file_tree=generate_file_tree()) self.send_response(200) self.send_header("Content-Type", "text/html") self.end_headers() self.wfile.write(html.encode("utf-8")) else: self.send_response(404) self.end_headers() self.wfile.write(b"404 Not Found") # ----------------------------- # Main # ----------------------------- if __name__ == "__main__": start_watcher() with socketserver.TCPServer(("", PORT), Handler) as httpd: print(f"GemLive running at http://localhost:{PORT}") httpd.serve_forever()