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 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

+ + + + + + + + + + + + + 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(-) 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(-) 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 - +