#!/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 = ["
"]
for path in sorted(ROOT.rglob("*")):
if path.suffix in VALID_SUFFIXES:
rel = path.relative_to(ROOT)
url = "/" + urllib.parse.quote(str(rel)) # encode spaces/special chars
html.append(f"- {rel}
")
html.append("
")
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()