From bd1e20b26c0f1ff7c1fd6f3b9bc34e32b5e067c9 Mon Sep 17 00:00:00 2001
From: kj_sh604
Date: Fri, 13 Feb 2026 22:47:17 -0500
Subject: feat: try making demoware for this
---
demoware/.gitignore | 2 +
demoware/index.html | 297 ++++++++++++++++++++++++++++++++++++++++++++++++++++
demoware/server.py | 210 +++++++++++++++++++++++++++++++++++++
3 files changed, 509 insertions(+)
create mode 100644 demoware/.gitignore
create mode 100644 demoware/index.html
create mode 100644 demoware/server.py
(limited to 'demoware')
diff --git a/demoware/.gitignore b/demoware/.gitignore
new file mode 100644
index 0000000..390f45d
--- /dev/null
+++ b/demoware/.gitignore
@@ -0,0 +1,2 @@
+uploads/
+output/
diff --git a/demoware/index.html b/demoware/index.html
new file mode 100644
index 0000000..c882c8c
--- /dev/null
+++ b/demoware/index.html
@@ -0,0 +1,297 @@
+
+
+
+
+
+ slide merging demoware
+
+
+
+
+ an attempt at a .pptx merging tool
+ merge multiple .pptx files into a single .pptx file.
+
+
+ 1. add files
+
+ drop .pptx files here — or click to browse
+
+
+
+ 2. merge order
+ no files added yet.
+
+
+ 3. merge
+
+
+
+
kjandoc command being used:
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demoware/server.py b/demoware/server.py
new file mode 100644
index 0000000..a4b1029
--- /dev/null
+++ b/demoware/server.py
@@ -0,0 +1,210 @@
+#!/usr/bin/env python3
+
+# kjandoc demoware server
+# a tiny threaded http server for the web interface.
+#
+# usage: python server.py [port]
+# default port: 8080
+
+import os
+import sys
+import json
+import uuid
+import time
+import shutil
+import threading
+import subprocess
+from http.server import HTTPServer, SimpleHTTPRequestHandler
+from socketserver import ThreadingMixIn
+from pathlib import Path
+from urllib.parse import unquote
+
+
+PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8080
+BASE_DIR = Path(__file__).parent.resolve()
+UPLOAD_DIR = BASE_DIR / 'uploads'
+OUTPUT_DIR = BASE_DIR / 'output'
+KJANDOC = (BASE_DIR.parent / 'src' / 'kjandoc').resolve()
+
+UPLOAD_DIR.mkdir(exist_ok=True)
+OUTPUT_DIR.mkdir(exist_ok=True)
+
+# {job_id: {status, command, output, log}}
+jobs = {}
+jobs_lock = threading.Lock()
+
+
+class ThreadedServer(ThreadingMixIn, HTTPServer):
+ daemon_threads = True
+
+
+class Handler(SimpleHTTPRequestHandler):
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, directory=str(BASE_DIR), **kwargs)
+
+ # -- routing --
+
+ def do_POST(self):
+ if self.path == '/api/upload':
+ self._upload()
+ elif self.path == '/api/merge':
+ self._merge()
+ else:
+ self.send_error(404)
+
+ def do_GET(self):
+ if self.path.startswith('/api/status/'):
+ self._status()
+ else:
+ super().do_GET()
+
+ # -- helpers --
+
+ def _json(self, code, data):
+ body = json.dumps(data).encode('utf-8')
+ self.send_response(code)
+ self.send_header('Content-Type', 'application/json')
+ self.send_header('Content-Length', str(len(body)))
+ self.end_headers()
+ self.wfile.write(body)
+
+ # -- handlers --
+
+ def _upload(self):
+ job_id = self.headers.get('X-Job-Id', '')
+ raw_name = self.headers.get('X-Filename', '')
+ length = int(self.headers.get('Content-Length', 0))
+
+ if not job_id or not raw_name:
+ return self._json(400, {"error": "missing headers"})
+ if length <= 0:
+ return self._json(400, {"error": "empty file"})
+
+ name = Path(unquote(raw_name)).name
+ if not name.lower().endswith('.pptx'):
+ return self._json(400, {"error": "not a .pptx file"})
+
+ job_dir = UPLOAD_DIR / job_id
+ job_dir.mkdir(parents=True, exist_ok=True)
+ dest = job_dir / name
+
+ with open(dest, 'wb') as f:
+ left = length
+ while left > 0:
+ chunk = self.rfile.read(min(left, 65536))
+ if not chunk:
+ break
+ f.write(chunk)
+ left -= len(chunk)
+
+ self._json(200, {"ok": True, "file": name})
+
+ def _merge(self):
+ length = int(self.headers.get('Content-Length', 0))
+ raw = self.rfile.read(length)
+ try:
+ data = json.loads(raw)
+ except json.JSONDecodeError:
+ return self._json(400, {"error": "bad json"})
+
+ job_id = data.get('job_id', '')
+ files = data.get('files', [])
+
+ if not job_id or not files:
+ return self._json(400, {"error": "missing job_id or files"})
+
+ job_dir = UPLOAD_DIR / job_id
+ if not job_dir.exists():
+ return self._json(400, {"error": "upload dir not found — did you upload first?"})
+
+ # validate filenames (no path traversal)
+ for f in files:
+ if '/' in f or '\\' in f or '..' in f:
+ return self._json(400, {"error": f"bad filename: {f}"})
+ if not (job_dir / f).exists():
+ return self._json(400, {"error": f"not found: {f}"})
+
+ # output: epoch_shortid.pptx
+ epoch = int(time.time())
+ uid = uuid.uuid4().hex[:8]
+ out_name = f"{epoch}_{uid}.pptx"
+ out_path = OUTPUT_DIR / out_name
+
+ # build the real command
+ inputs = [str(job_dir / f) for f in files]
+ cmd = [sys.executable, str(KJANDOC)] + inputs + ['-o', str(out_path)]
+
+ # pretty command for display (strip numeric id prefixes like "3_")
+ pretty_names = []
+ for f in files:
+ parts = f.split('_', 1)
+ pretty_names.append(parts[1] if len(parts) == 2 and parts[0].isdigit() else f)
+ cmd_str = f"kjandoc {' '.join(pretty_names)} -o output/{out_name}"
+
+ with jobs_lock:
+ jobs[job_id] = {
+ "status": "running",
+ "command": cmd_str,
+ "output": out_name,
+ "log": "",
+ }
+
+ def do_merge():
+ try:
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
+ log = result.stdout + result.stderr
+ status = "done" if result.returncode == 0 else "error"
+ except subprocess.TimeoutExpired:
+ log = "timed out (10 min limit)"
+ status = "error"
+ except Exception as e:
+ log = str(e)
+ status = "error"
+
+ with jobs_lock:
+ jobs[job_id]["log"] = log
+ jobs[job_id]["status"] = status
+
+ # clean up uploads for this job
+ shutil.rmtree(job_dir, ignore_errors=True)
+
+ threading.Thread(target=do_merge, daemon=True).start()
+
+ self._json(200, {
+ "ok": True,
+ "job_id": job_id,
+ "command": cmd_str,
+ "output": out_name,
+ })
+
+ def _status(self):
+ job_id = self.path.rsplit('/', 1)[-1]
+ with jobs_lock:
+ job = jobs.get(job_id)
+ snapshot = dict(job) if job else None
+ if not snapshot:
+ return self._json(404, {"error": "unknown job"})
+ self._json(200, snapshot)
+
+
+def main():
+ if not KJANDOC.exists():
+ print(f"[!] kjandoc not found: {KJANDOC}", file=sys.stderr)
+ print(f" expected at ../src/kjandoc relative to this script", file=sys.stderr)
+ sys.exit(1)
+
+ srv = ThreadedServer(('', PORT), Handler)
+ print(f"[*] kjandoc demoware")
+ print(f"[*] http://localhost:{PORT}")
+ print(f"[*] kjandoc: {KJANDOC}")
+ print(f"[*] ctrl-c to stop")
+ try:
+ srv.serve_forever()
+ except KeyboardInterrupt:
+ print("\n[*] bye")
+ srv.shutdown()
+
+
+if __name__ == '__main__':
+ main()
--
cgit v1.2.3
From 58aea46fa3b892bf12ee16ccb505099ac578fd82 Mon Sep 17 00:00:00 2001
From: kj_sh604
Date: Fri, 13 Feb 2026 23:15:17 -0500
Subject: refactor: use PATH kjandoc
---
demoware/server.py | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
(limited to 'demoware')
diff --git a/demoware/server.py b/demoware/server.py
index a4b1029..949edb8 100644
--- a/demoware/server.py
+++ b/demoware/server.py
@@ -24,7 +24,6 @@ PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8080
BASE_DIR = Path(__file__).parent.resolve()
UPLOAD_DIR = BASE_DIR / 'uploads'
OUTPUT_DIR = BASE_DIR / 'output'
-KJANDOC = (BASE_DIR.parent / 'src' / 'kjandoc').resolve()
UPLOAD_DIR.mkdir(exist_ok=True)
OUTPUT_DIR.mkdir(exist_ok=True)
@@ -133,7 +132,7 @@ class Handler(SimpleHTTPRequestHandler):
# build the real command
inputs = [str(job_dir / f) for f in files]
- cmd = [sys.executable, str(KJANDOC)] + inputs + ['-o', str(out_path)]
+ cmd = ['kjandoc'] + inputs + ['-o', str(out_path)]
# pretty command for display (strip numeric id prefixes like "3_")
pretty_names = []
@@ -189,15 +188,16 @@ class Handler(SimpleHTTPRequestHandler):
def main():
- if not KJANDOC.exists():
- print(f"[!] kjandoc not found: {KJANDOC}", file=sys.stderr)
- print(f" expected at ../src/kjandoc relative to this script", file=sys.stderr)
+ kjandoc_path = shutil.which('kjandoc')
+ if not kjandoc_path:
+ print(f"[!] kjandoc not found in PATH", file=sys.stderr)
+ print(f" install kjandoc or ensure it's available in your PATH", file=sys.stderr)
sys.exit(1)
srv = ThreadedServer(('', PORT), Handler)
print(f"[*] kjandoc demoware")
print(f"[*] http://localhost:{PORT}")
- print(f"[*] kjandoc: {KJANDOC}")
+ print(f"[*] kjandoc: {kjandoc_path}")
print(f"[*] ctrl-c to stop")
try:
srv.serve_forever()
--
cgit v1.2.3
From 1f1abae197f3fd58da2c3ab7a0a198bec3bbb847 Mon Sep 17 00:00:00 2001
From: kj_sh604
Date: Fri, 13 Feb 2026 23:32:50 -0500
Subject: refactor: make the site work look ok in both light and dark modes
---
demoware/index.html | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
(limited to 'demoware')
diff --git a/demoware/index.html b/demoware/index.html
index c882c8c..95c6847 100644
--- a/demoware/index.html
+++ b/demoware/index.html
@@ -4,7 +4,7 @@
slide merging demoware
-
+