diff --git a/hermes-custom.css b/hermes-custom.css index b0d46ab..545eb34 100644 --- a/hermes-custom.css +++ b/hermes-custom.css @@ -1,249 +1 @@ -/* HermesCustom CSS — Lukas' responsive-layout + Mobile-Toggle-Pattern. - * - * Loaded via in vault.html. - * Applied at first paint (no FOUC). - * - * LAYOUT PHILOSOPHY (v0.5): - * - Desktop ≥1025px: 3-panel layout, tree visible, graph visible - * - Tablet 600-1024px: 2-panel compact, tree visible OR graph visible - * via "Show Graph" button toggle (starts hidden) - * - Mobile <600px: single column, tree as hamburger, graph as FAB → modal - * - * The key UX change vs v0.4: Graph is NEVER in bottom panel on Mobile - * (waste of vertical space). It's a toggle button → full-screen modal. - */ - -/* === Always-on === */ -:root { - /* iOS safe-area inset variables for cross-browser usage */ - --sat: env(safe-area-inset-top, 0px); - --sab: env(safe-area-inset-bottom, 0px); - --sal: env(safe-area-inset-left, 0px); - --sar: env(safe-area-inset-right, 0px); -} - -html, body { - /* Prevent iOS from auto-shrinking — keep our viewport stable */ - height: 100%; - overflow: hidden; - box-sizing: border-box; - margin: 0; - padding: 0; -} - -.vault-app.hermes-rc, -.vault-app.hermes-rc * { - box-sizing: border-box !important; -} -.vault-app.hermes-rc { - /* Use dvh (dynamic viewport height) with vh fallback. dvh excludes iOS - URL-bar from the 100vh, so the app fills the visible area. - Padding-top pushes the grid below the iOS status bar / Dynamic Island. */ - width: 100% !important; - max-width: 100vw !important; - height: 100vh !important; /* fallback */ - height: 100dvh !important; /* modern browsers (incl. iOS 16+) */ - padding-top: var(--sat) !important; - padding-left: var(--sal) !important; - padding-right: var(--sar) !important; - padding-bottom: var(--sab) !important; - overflow: hidden !important; - overflow-x: hidden !important; -} -.vault-app.hermes-rc .vault-content { - overflow-x: hidden !important; - overflow-y: auto !important; - max-width: 100% !important; - word-wrap: break-word !important; - overflow-wrap: break-word !important; - hyphens: auto !important; - min-width: 0 !important; -} -/* Critical: prevent pre/code blocks from overflowing horizontally */ -.vault-app.hermes-rc .vault-content pre, -.vault-app.hermes-rc .vault-content code { - white-space: pre-wrap !important; - word-break: break-word !important; - max-width: 100% !important; - overflow-x: auto !important; -} -.vault-app.hermes-rc .vault-content table { - display: block !important; - overflow-x: auto !important; -} -.vault-app.hermes-rc .vault-content img { - max-width: 100% !important; - height: auto !important; -} -.vault-app.hermes-rc .vault-sidebar { - overflow-x: hidden !important; - overflow-y: auto !important; -} -.vault-app.hermes-rc .vault-graph { - position: relative !important; - overflow: hidden !important; -} - -/* === HermescCustom UI Elements === */ -.hermes-ui-btn { - background: var(--bg-secondary); - border: 1px solid var(--border); - color: var(--text-secondary); - padding: 6px 12px; - border-radius: 4px; - cursor: pointer; - font-size: 14px; - line-height: 1; - margin: 0 4px; - transition: color 0.15s, border-color 0.15s; -} -.hermes-ui-btn:hover { - color: var(--accent); - border-color: var(--accent); -} - -/* Hamburger button (hidden by default, shown when tree hidden) */ -.hermes-tree-toggle, -.hermes-graph-toggle { - display: none; -} -.hermes-tree-toggle svg, -.hermes-graph-toggle svg { - width: 18px; - height: 18px; - vertical-align: middle; -} - -/* Graph Modal (mobile graph fullscreen overlay) */ -.hermes-graph-modal { - display: none; - position: fixed; - top: 48px; - left: 0; - right: 0; - bottom: 0; - background: var(--bg-primary); - z-index: 2000; - flex-direction: column; -} -.hermes-graph-modal.hermes-modal-open { - display: flex; -} -.hermes-graph-modal-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 8px 16px; - background: var(--bg-secondary); - border-bottom: 1px solid var(--border); - height: 44px; - flex-shrink: 0; -} -.hermes-graph-modal-title { - color: var(--accent); - font-size: 13px; - font-weight: 700; - letter-spacing: 0.5px; -} -.hermes-graph-modal-close { - background: none; - border: 1px solid var(--border); - color: var(--text-secondary); - padding: 4px 12px; - border-radius: 4px; - cursor: pointer; - font-size: 14px; -} -.hermes-graph-modal-close:hover { - color: var(--accent); - border-color: var(--accent); -} -.hermes-graph-modal-canvas { - flex: 1; - position: relative; - overflow: hidden; -} -.hermes-graph-modal-canvas canvas { - display: block; - width: 100% !important; - height: 100% !important; -} - -/* Sidebar backdrop (click-outside-to-close) */ -.hermes-sidebar-backdrop { - display: none; - position: fixed; - top: 48px; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - z-index: 900; -} -.hermes-backdrop-visible { - display: block; -} - -/* === Compact Mode (≤1024px) — used for Tablet + Mobile === */ -@media (max-width: 1024px) { - .vault-app.hermes-rc.hermes-compact { - grid-template-columns: 220px 1fr !important; - grid-template-rows: 48px 1fr !important; - height: 100vh !important; - } - .vault-app.hermes-rc.hermes-compact .vault-sidebar { - width: 220px !important; - max-width: 220px !important; - } - .vault-app.hermes-rc.hermes-compact .vault-graph { - display: none !important; /* Hidden by default on compact */ - } - /* Show tree-toggle in compact (tree is shown by default; toggle if user wants to hide) */ - .vault-app.hermes-rc.hermes-compact .hermes-tree-toggle { - display: inline-block !important; - } - /* Show graph-toggle in compact */ - .vault-app.hermes-rc.hermes-compact .hermes-graph-toggle { - display: inline-block !important; - } -} - -/* === Mobile Mode (≤600px) — Full mobile UX === */ -@media (max-width: 600px) { - .vault-app.hermes-rc.hermes-mobile { - grid-template-columns: 1fr !important; - grid-template-rows: 48px 1fr !important; - } - .vault-app.hermes-rc.hermes-mobile .vault-sidebar { - position: fixed !important; - top: 48px !important; - left: 0 !important; - bottom: 0 !important; - width: 85vw !important; - max-width: 320px !important; - transform: translateX(-100%) !important; - transition: transform 0.25s ease-in-out !important; - z-index: 1000 !important; - background: var(--bg-secondary) !important; - border-right: 1px solid var(--border) !important; - display: block !important; - height: calc(100vh - 48px) !important; - } - .vault-app.hermes-rc.hermes-mobile .vault-sidebar.hermes-sidebar-open { - transform: translateX(0) !important; - } - .vault-app.hermes-rc.hermes-mobile .vault-content { - grid-column: 1 !important; - grid-row: 2 !important; - padding: 14px 16px !important; - max-width: 100% !important; - } -} - -/* === Desktop (>=1025px) — default obv layout, no overrides === */ -@media (min-width: 1025px) { - .vault-app.hermes-rc.hermes-desktop .hermes-tree-toggle, - .vault-app.hermes-rc.hermes-desktop .hermes-graph-toggle { - display: none !important; - } -} +/* placeholder */ diff --git a/hermes-custom.css.lukas b/hermes-custom.css.lukas new file mode 100644 index 0000000..b0d46ab --- /dev/null +++ b/hermes-custom.css.lukas @@ -0,0 +1,249 @@ +/* HermesCustom CSS — Lukas' responsive-layout + Mobile-Toggle-Pattern. + * + * Loaded via in vault.html. + * Applied at first paint (no FOUC). + * + * LAYOUT PHILOSOPHY (v0.5): + * - Desktop ≥1025px: 3-panel layout, tree visible, graph visible + * - Tablet 600-1024px: 2-panel compact, tree visible OR graph visible + * via "Show Graph" button toggle (starts hidden) + * - Mobile <600px: single column, tree as hamburger, graph as FAB → modal + * + * The key UX change vs v0.4: Graph is NEVER in bottom panel on Mobile + * (waste of vertical space). It's a toggle button → full-screen modal. + */ + +/* === Always-on === */ +:root { + /* iOS safe-area inset variables for cross-browser usage */ + --sat: env(safe-area-inset-top, 0px); + --sab: env(safe-area-inset-bottom, 0px); + --sal: env(safe-area-inset-left, 0px); + --sar: env(safe-area-inset-right, 0px); +} + +html, body { + /* Prevent iOS from auto-shrinking — keep our viewport stable */ + height: 100%; + overflow: hidden; + box-sizing: border-box; + margin: 0; + padding: 0; +} + +.vault-app.hermes-rc, +.vault-app.hermes-rc * { + box-sizing: border-box !important; +} +.vault-app.hermes-rc { + /* Use dvh (dynamic viewport height) with vh fallback. dvh excludes iOS + URL-bar from the 100vh, so the app fills the visible area. + Padding-top pushes the grid below the iOS status bar / Dynamic Island. */ + width: 100% !important; + max-width: 100vw !important; + height: 100vh !important; /* fallback */ + height: 100dvh !important; /* modern browsers (incl. iOS 16+) */ + padding-top: var(--sat) !important; + padding-left: var(--sal) !important; + padding-right: var(--sar) !important; + padding-bottom: var(--sab) !important; + overflow: hidden !important; + overflow-x: hidden !important; +} +.vault-app.hermes-rc .vault-content { + overflow-x: hidden !important; + overflow-y: auto !important; + max-width: 100% !important; + word-wrap: break-word !important; + overflow-wrap: break-word !important; + hyphens: auto !important; + min-width: 0 !important; +} +/* Critical: prevent pre/code blocks from overflowing horizontally */ +.vault-app.hermes-rc .vault-content pre, +.vault-app.hermes-rc .vault-content code { + white-space: pre-wrap !important; + word-break: break-word !important; + max-width: 100% !important; + overflow-x: auto !important; +} +.vault-app.hermes-rc .vault-content table { + display: block !important; + overflow-x: auto !important; +} +.vault-app.hermes-rc .vault-content img { + max-width: 100% !important; + height: auto !important; +} +.vault-app.hermes-rc .vault-sidebar { + overflow-x: hidden !important; + overflow-y: auto !important; +} +.vault-app.hermes-rc .vault-graph { + position: relative !important; + overflow: hidden !important; +} + +/* === HermescCustom UI Elements === */ +.hermes-ui-btn { + background: var(--bg-secondary); + border: 1px solid var(--border); + color: var(--text-secondary); + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + line-height: 1; + margin: 0 4px; + transition: color 0.15s, border-color 0.15s; +} +.hermes-ui-btn:hover { + color: var(--accent); + border-color: var(--accent); +} + +/* Hamburger button (hidden by default, shown when tree hidden) */ +.hermes-tree-toggle, +.hermes-graph-toggle { + display: none; +} +.hermes-tree-toggle svg, +.hermes-graph-toggle svg { + width: 18px; + height: 18px; + vertical-align: middle; +} + +/* Graph Modal (mobile graph fullscreen overlay) */ +.hermes-graph-modal { + display: none; + position: fixed; + top: 48px; + left: 0; + right: 0; + bottom: 0; + background: var(--bg-primary); + z-index: 2000; + flex-direction: column; +} +.hermes-graph-modal.hermes-modal-open { + display: flex; +} +.hermes-graph-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 16px; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border); + height: 44px; + flex-shrink: 0; +} +.hermes-graph-modal-title { + color: var(--accent); + font-size: 13px; + font-weight: 700; + letter-spacing: 0.5px; +} +.hermes-graph-modal-close { + background: none; + border: 1px solid var(--border); + color: var(--text-secondary); + padding: 4px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; +} +.hermes-graph-modal-close:hover { + color: var(--accent); + border-color: var(--accent); +} +.hermes-graph-modal-canvas { + flex: 1; + position: relative; + overflow: hidden; +} +.hermes-graph-modal-canvas canvas { + display: block; + width: 100% !important; + height: 100% !important; +} + +/* Sidebar backdrop (click-outside-to-close) */ +.hermes-sidebar-backdrop { + display: none; + position: fixed; + top: 48px; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 900; +} +.hermes-backdrop-visible { + display: block; +} + +/* === Compact Mode (≤1024px) — used for Tablet + Mobile === */ +@media (max-width: 1024px) { + .vault-app.hermes-rc.hermes-compact { + grid-template-columns: 220px 1fr !important; + grid-template-rows: 48px 1fr !important; + height: 100vh !important; + } + .vault-app.hermes-rc.hermes-compact .vault-sidebar { + width: 220px !important; + max-width: 220px !important; + } + .vault-app.hermes-rc.hermes-compact .vault-graph { + display: none !important; /* Hidden by default on compact */ + } + /* Show tree-toggle in compact (tree is shown by default; toggle if user wants to hide) */ + .vault-app.hermes-rc.hermes-compact .hermes-tree-toggle { + display: inline-block !important; + } + /* Show graph-toggle in compact */ + .vault-app.hermes-rc.hermes-compact .hermes-graph-toggle { + display: inline-block !important; + } +} + +/* === Mobile Mode (≤600px) — Full mobile UX === */ +@media (max-width: 600px) { + .vault-app.hermes-rc.hermes-mobile { + grid-template-columns: 1fr !important; + grid-template-rows: 48px 1fr !important; + } + .vault-app.hermes-rc.hermes-mobile .vault-sidebar { + position: fixed !important; + top: 48px !important; + left: 0 !important; + bottom: 0 !important; + width: 85vw !important; + max-width: 320px !important; + transform: translateX(-100%) !important; + transition: transform 0.25s ease-in-out !important; + z-index: 1000 !important; + background: var(--bg-secondary) !important; + border-right: 1px solid var(--border) !important; + display: block !important; + height: calc(100vh - 48px) !important; + } + .vault-app.hermes-rc.hermes-mobile .vault-sidebar.hermes-sidebar-open { + transform: translateX(0) !important; + } + .vault-app.hermes-rc.hermes-mobile .vault-content { + grid-column: 1 !important; + grid-row: 2 !important; + padding: 14px 16px !important; + max-width: 100% !important; + } +} + +/* === Desktop (>=1025px) — default obv layout, no overrides === */ +@media (min-width: 1025px) { + .vault-app.hermes-rc.hermes-desktop .hermes-tree-toggle, + .vault-app.hermes-rc.hermes-desktop .hermes-graph-toggle { + display: none !important; + } +} diff --git a/server.py b/server.py index d2f3c26..a171ad5 100644 --- a/server.py +++ b/server.py @@ -217,17 +217,8 @@ class VaultHandler(BaseHTTPRequestHandler): params = urllib.parse.parse_qs(query_string) q = params.get("q", [""])[0] self._json({"results": _search(VAULT_DIR, q) if q else []}) - elif path == "/api/vault/file" or path.startswith("/api/vault/file/"): - if path == "/api/vault/file": - rel = "" - else: - rel = unquote(path[len("/api/vault/file/"):]) - # Lukas-add: also support ?file= query for clean URL navigation - if not rel and query_string: - from urllib.parse import parse_qs - file_param = parse_qs(query_string).get("file", [None])[0] - if file_param: - rel = file_param + elif path.startswith("/api/vault/file/"): + rel = unquote(path[len("/api/vault/file/"):]) file_path = VAULT_DIR / rel if not file_path.exists() or not str(file_path).startswith(str(VAULT_DIR)): self._json({"error": "Not found"}, 404) diff --git a/server.py.lukas b/server.py.lukas new file mode 100644 index 0000000..d2f3c26 --- /dev/null +++ b/server.py.lukas @@ -0,0 +1,302 @@ +""" +Obsidian Web Viewer — Browse Your Vault in the Browser +======================================================== +Web-based Obsidian vault browser with 3D graph, file tree, markdown +rendering, wikilink navigation, and full-text search. + +Usage: + python server.py --vault /path/to/your/vault + python server.py --vault ~/Documents/MyVault --port 8080 +""" +import sys +import os +import json +import re +import argparse +import time +import threading +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from datetime import datetime +from urllib.parse import unquote, quote + +if sys.platform == 'win32': + try: + sys.stdout.reconfigure(encoding='utf-8') + except Exception: + pass + +ROOT = Path(__file__).parent +VAULT_DIR = None # Set via --vault CLI arg + +# YAML parsing (optional) +_YAML_AVAILABLE = False +try: + import yaml + _YAML_AVAILABLE = True +except ImportError: + pass + +MIME = { + ".html": "text/html; charset=utf-8", + ".css": "text/css", + ".js": "application/javascript", + ".json": "application/json", + ".png": "image/png", + ".jpg": "image/jpeg", + ".svg": "image/svg+xml", +} + +# Cache for vault tree and graph +_cache = {"tree": None, "graph": None, "ts": 0} +_CACHE_TTL = 300 + + +def _build_tree(vault_dir: Path) -> dict: + """Build a JSON file tree of the vault directory.""" + now = time.time() + if _cache["tree"] and (now - _cache["ts"]) < _CACHE_TTL: + return _cache["tree"] + + def _walk(d: Path) -> dict: + node = {"type": "folder", "name": d.name, "children": []} + try: + entries = sorted(d.iterdir(), key=lambda e: (not e.is_dir(), e.name.lower())) + except PermissionError: + return node + for entry in entries: + if entry.name.startswith('.') or entry.name.startswith('_'): + continue + if entry.is_dir(): + child = _walk(entry) + if child["children"]: + node["children"].append(child) + elif entry.suffix.lower() == '.md': + node["children"].append({"type": "file", "name": entry.name}) + return node + + if not vault_dir.exists(): + return {"type": "folder", "name": "", "children": []} + tree = _walk(vault_dir) + tree["name"] = "" + _cache["tree"] = tree + _cache["ts"] = time.time() + return tree + + +def _parse_frontmatter(raw: str) -> tuple: + """Parse YAML frontmatter from markdown. Returns (dict, body_str).""" + if not raw.startswith('---'): + return {}, raw + end = raw.find('---', 3) + if end == -1: + return {}, raw + fm_text = raw[3:end].strip() + body = raw[end + 3:].strip() + fm = {} + if _YAML_AVAILABLE: + try: + fm = yaml.safe_load(fm_text) or {} + if not isinstance(fm, dict): + fm = {"value": fm} + except Exception: + fm = {} + else: + for line in fm_text.splitlines(): + if ':' in line: + k, _, v = line.partition(':') + v = v.strip().strip('"').strip("'") + if v.startswith('[') and v.endswith(']'): + v = [x.strip().strip('"').strip("'") for x in v[1:-1].split(',') if x.strip()] + fm[k.strip()] = v + return fm, body + + +def _search(vault_dir: Path, query: str, max_results: int = 30) -> list: + """Search vault files by name and content.""" + results = [] + query_lower = query.lower() + for md_file in vault_dir.rglob("*.md"): + if md_file.name.startswith('.') or md_file.name.startswith('_'): + continue + try: + rel = md_file.relative_to(vault_dir).as_posix() + except ValueError: + continue + name = md_file.stem + name_match = query_lower in name.lower() + snippet = "" + content_match = False + if not name_match: + try: + text = md_file.read_text(encoding="utf-8", errors="ignore")[:5000] + idx = text.lower().find(query_lower) + if idx >= 0: + content_match = True + start = max(0, idx - 40) + end = min(len(text), idx + len(query) + 60) + snippet = ("..." if start > 0 else "") + text[start:end].replace("\n", " ") + ("..." if end < len(text) else "") + except Exception: + continue + if name_match or content_match: + results.append({ + "path": rel, "name": name, + "folder": str(md_file.parent.relative_to(vault_dir)) if md_file.parent != vault_dir else "", + "snippet": snippet, "name_match": name_match, + }) + if len(results) >= max_results: + break + results.sort(key=lambda r: (not r["name_match"], r["name"].lower())) + return results + + +def _build_graph(vault_dir: Path) -> dict: + """Build a graph of wikilink connections between vault notes.""" + if _cache["graph"] and (time.time() - _cache["ts"]) < _CACHE_TTL: + return _cache["graph"] + + wikilink_re = re.compile(r'\[\[([^\]|]+?)(?:\|[^\]]+?)?\]\]') + nodes = [] + edges = [] + name_to_path = {} + all_paths = [] + + for md_file in vault_dir.rglob("*.md"): + if md_file.name.startswith('.') or md_file.name.startswith('_'): + continue + try: + rel = md_file.relative_to(vault_dir).as_posix() + except ValueError: + continue + all_paths.append((rel, md_file)) + name_to_path[md_file.stem.lower()] = rel + + for rel, md_file in all_paths: + try: + text = md_file.read_text(encoding="utf-8", errors="ignore") + except Exception: + text = "" + + # Classify by folder for coloring + rp = rel.lower() + node_type = "other" + for folder in ["wiki", "reference", "daily", "journal", "notes", "projects"]: + if rp.startswith(folder + "/"): + node_type = folder + break + + nodes.append({"id": rel, "label": md_file.stem, "type": node_type}) + links = wikilink_re.findall(text) + seen = set() + for target in links: + resolved = name_to_path.get(target.strip().lower()) + if resolved and resolved != rel and resolved not in seen: + edges.append({"source": rel, "target": resolved}) + seen.add(resolved) + + result = {"nodes": nodes, "edges": edges} + _cache["graph"] = result + return result + + +class VaultHandler(BaseHTTPRequestHandler): + def log_message(self, *a): pass + + def do_GET(self): + path = self.path.split("?")[0].rstrip("/") + query_string = self.path.split("?")[1] if "?" in self.path else "" + + if path == "" or path == "/" or path == "/vault": + self._serve_file(ROOT / "vault.html") + elif path == "/api/vault/tree": + self._json(_build_tree(VAULT_DIR)) + elif path == "/api/vault/graph": + self._json(_build_graph(VAULT_DIR)) + elif path == "/api/vault/search": + import urllib.parse + params = urllib.parse.parse_qs(query_string) + q = params.get("q", [""])[0] + self._json({"results": _search(VAULT_DIR, q) if q else []}) + elif path == "/api/vault/file" or path.startswith("/api/vault/file/"): + if path == "/api/vault/file": + rel = "" + else: + rel = unquote(path[len("/api/vault/file/"):]) + # Lukas-add: also support ?file= query for clean URL navigation + if not rel and query_string: + from urllib.parse import parse_qs + file_param = parse_qs(query_string).get("file", [None])[0] + if file_param: + rel = file_param + file_path = VAULT_DIR / rel + if not file_path.exists() or not str(file_path).startswith(str(VAULT_DIR)): + self._json({"error": "Not found"}, 404) + return + try: + raw = file_path.read_text(encoding="utf-8") + fm, body = _parse_frontmatter(raw) + self._json({"path": rel, "name": file_path.stem, "frontmatter": fm, "body": body}) + except Exception as e: + self._json({"error": str(e)}, 500) + elif path == "/config.json": + self._serve_file(ROOT / "config.json") + else: + f = ROOT / path.lstrip("/") + if f.exists() and f.is_file(): + self._serve_file(f) + else: + self.send_error(404) + + def _serve_file(self, path): + if not path.exists(): + self.send_error(404); return + data = path.read_bytes() + mime = MIME.get(path.suffix.lower(), "application/octet-stream") + self.send_response(200) + self.send_header("Content-Type", mime) + self.send_header("Content-Length", str(len(data))) + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Cache-Control", "no-cache") + self.end_headers() + self.wfile.write(data) + + def _json(self, obj, code=200): + data = json.dumps(obj, indent=2, default=str).encode() + self.send_response(code) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(data))) + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Cache-Control", "no-cache") + self.end_headers() + self.wfile.write(data) + + +def main(): + parser = argparse.ArgumentParser(description="Obsidian Web Viewer") + parser.add_argument("--vault", required=True, help="Path to your Obsidian vault folder") + parser.add_argument("--port", type=int, default=8765) + parser.add_argument("--host", default="0.0.0.0") + args = parser.parse_args() + + global VAULT_DIR + VAULT_DIR = Path(args.vault).resolve() + + if not VAULT_DIR.exists(): + print(f"ERROR: Vault path does not exist: {VAULT_DIR}") + sys.exit(1) + + md_count = sum(1 for _ in VAULT_DIR.rglob("*.md")) + print(f"Obsidian Web Viewer") + print(f" Vault: {VAULT_DIR}") + print(f" Notes: {md_count}") + print(f" URL: http://{args.host}:{args.port}") + + server = ThreadingHTTPServer((args.host, args.port), VaultHandler) + try: + server.serve_forever() + except KeyboardInterrupt: + print("\nShutting down.") + + +if __name__ == "__main__": + main() diff --git a/vault-custom.js b/vault-custom.js index 8ea4b9b..491a83c 100644 --- a/vault-custom.js +++ b/vault-custom.js @@ -1,453 +1 @@ -/* HermesCustom — Lukas' additions to obsidian-web-viewer - * - * Loaded by vault.html via + + + +
+
+

Vault

+ + +
+ + + +
+
+

Welcome to Vault Viewer

+

Select a note from the file tree or use search.

+
+
+ +
+ +
+
+ +
+ + + + +