Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3f04df7
feat: add interactive web treemap via `dirplot serve`
deeplook Jun 2, 2026
cd19988
fix: correct breadcrumb trail when zooming into deeply nested dirs
deeplook Jun 2, 2026
cd59a0a
feat(serve): add Settings/Metrics/Preview tab panel to sidebar
deeplook Jun 2, 2026
f30b22d
fix(serve): fix file preview path resolution and auto-expand sidebar
deeplook Jun 2, 2026
53a8c1e
fix: use browser-ready @highlightjs/cdn-assets for syntax highlightin…
deeplook Jun 2, 2026
1d5a251
feat(serve): show dirplot metadata in image/video previews
deeplook Jun 3, 2026
e5928ec
feat(serve): show hex dump of first 1000 bytes for binary file previews
deeplook Jun 3, 2026
ff67f8b
feat(serve): add PDF preview via iframe
deeplook Jun 3, 2026
ffe39d1
chore: add TODO for PDF metadata embedding via pypdf
deeplook Jun 3, 2026
971e210
feat(serve): add search icon; preview tab no longer auto-activated on…
deeplook Jun 3, 2026
0cea344
feat(serve): remove Apply button; all settings changes apply instantly
deeplook Jun 3, 2026
e05484b
feat(serve): add regex toggle to search; highlights invalid patterns
deeplook Jun 3, 2026
a69cf29
feat(serve): add paired dark/light code theme selector to settings
deeplook Jun 3, 2026
8c899ae
feat(serve): expand code theme selector to 10 paired dark/light themes
deeplook Jun 3, 2026
8d2542a
feat(serve): add source input field to toolbar; breadcrumbs move righ…
deeplook Jun 3, 2026
fc3873f
feat(sources): register Docker, K8s, and S3 as first-class tree sources
deeplook Jun 3, 2026
3669a6c
feat(sources): support HTTP(S) URLs as archive sources; download to t…
deeplook Jun 3, 2026
d09db28
feat(sources): guard URL archive downloads with 100 MB size limit
deeplook Jun 3, 2026
a3c56a5
feat(serve): add keyboard navigation (j/k, Enter, Esc, /, Backspace)
deeplook Jun 3, 2026
538cfcc
docs: add web interface (dirplot serve) documentation page
deeplook Jun 3, 2026
dec7656
fix(serve): refresh metrics tab content automatically on rescan
deeplook Jun 3, 2026
02dc020
fix(serve): metrics endpoint respects current root, depth, and exclud…
deeplook Jun 3, 2026
50bd524
fix(serve): compute metrics client-side from tree data; block preview…
deeplook Jun 3, 2026
7126038
feat(serve): file preview, export, source history, and media support
deeplook Jun 3, 2026
156caa3
chore: bump version to 0.6.0 and update changelog
deeplook Jun 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.6.0] - 2026-06-09

### Added

- **`dirplot serve` — interactive web treemap (experimental, undocumented)** — new command that
starts a local FastAPI server and opens a D3.js treemap in the browser. This feature is still
under active development; the interface and API are subject to change in future releases, and
full documentation is forthcoming. Features include: zoomable tiles, breadcrumb navigation,
keyword and regex search, keyboard navigation (j/k, Enter, /, Esc), a sidebar with Settings,
Metrics, and Preview tabs, and instant (no Apply button) settings changes.

- **File preview in `serve`** — the Preview tab renders syntax-highlighted source files (10 paired
dark/light code themes), PDF documents via iframe, a hex dump for binary files, and image/video
previews with embedded dirplot metadata. HEIC images are supported via `pillow-heif` or the
macOS `sips` fallback.

- **Source input and history in `serve`** — a toolbar text field accepts any source supported by
`dirplot` (local path, archive, GitHub URL, Docker, K8s, S3). Previous sources are tracked in a
history dropdown.

- **HTTP(S) URL archive sources** — `dirplot` now accepts `https://` and `http://` URLs pointing
to archive files (zip, tar, etc.) as a source in any command. Downloads are capped at 100 MB.

- **Docker, Kubernetes, and S3 as first-class sources** — `docker://`, `k8s://`, and `s3://`
schemes are now registered in the global source registry, making them available wherever a
source path is accepted.

## [0.5.1] - 2026-05-28

### Fixed
Expand Down
14 changes: 14 additions & 0 deletions NEW-TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# TODO, Website

- [x] Add top left text field to enter data source (local, git, ...).
- [x] Place breadcrumps right to new data source text field.
- [x] Make search more useful (REGEX checkbox?).
- [x] Add search icon (lense) next to search text field..
- [x] Show only first 1000 bytes of binary files in preview.
- [x] When clicking on a file rectangle show preview only when preview tab is already open. Do not switch to the preview tab only when a rectangle is clicked.
- [x] Try to get rid of the "Apply" button, and apply all changes instantly.
- [ ] Open website (now --serve) also for diff mode, showing diffs in preview.
- [x] Make dependencies for serve core, not optional.
- [x] Show meta data in image/video previews.
- [ ] When creating PDF files, embed dirplot metadata (Date, Software, URL, Python, OS, Command) via PDF info dict / XMP; read it back in `_read_meta_from_file` using `pypdf`.
- [ ]
147 changes: 147 additions & 0 deletions docs/serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Web Interface (`dirplot serve`)

← [Home](index.md)

`dirplot serve` launches an interactive treemap in your browser. Unlike the static PNG/SVG output of `dirplot map`, the web interface lets you explore the tree live — zoom into directories, search, preview file contents, and tweak every setting without re-running the command.

```bash
dirplot serve .
dirplot serve /path/to/project
dirplot serve github://owner/repo
dirplot serve s3://my-bucket
```

The server starts on `http://localhost:8000` by default. Press `Ctrl-C` to stop it.

## Options

| Flag | Default | Description |
|---|---|---|
| `--port` | `8000` | TCP port to listen on |
| `--host` | `127.0.0.1` | Bind address (`0.0.0.0` to expose on LAN) |
| `--depth N` | unlimited | Initial recursion depth |
| `--colormap NAME` | `tab20` | Initial colormap |
| `--exclude PATTERN` | — | Glob pattern to skip (repeatable) |
| `--log-scale N` | `1` (off) | Initial log-scale strength (2–10) |
| `--allow-write` | off | Enable file delete/move via the context menu |

---

## Toolbar

The toolbar runs across the top of the page and contains three areas from left to right.

### Source input

The source field (left of the breadcrumbs) shows the current root. Type any supported path or URL and press **Enter** to rescan:

| Input | Example |
|---|---|
| Local directory | `/home/user/project` or `../sibling` |
| Archive (local) | `/tmp/release.tar.gz` |
| Archive (remote URL) | `https://example.com/archive.zip` |
| GitHub repository | `github://owner/repo` or `https://github.com/owner/repo` |
| SSH remote | `ssh://user@host/path` |
| AWS S3 | `s3://bucket/prefix` |
| Docker container | `docker://container/path` |
| Kubernetes pod | `pod://pod-name/path` or `pod://pod-name@namespace/path` |

> **Note:** Remote URL archives are downloaded to a temporary file (max 100 MB), scanned, then deleted immediately.

### Breadcrumbs

Shows your current zoom path inside the tree. Click any crumb to jump back to that level.

### Search

Type to highlight matching files. The `.*` pill to the right of the input toggles **regular expression** mode — the pill turns accent-coloured when active. Invalid regex patterns turn the input border red without crashing.

---

## Treemap

The main canvas renders the directory tree as a nested squarify treemap. Tile area is proportional to file size (or log-scaled size when log-scale > 1).

### Layout vs. static output

The web view and `dirplot map` both use the squarified treemap algorithm (Bruls et al. 2000) and both reserve the same header height for directory labels before running the layout. They still produce different tile arrangements because:

- **Different squarify implementations.** D3's `treemapSquarify` and the Python `squarify` package implement the same algorithm but differ in floating-point arithmetic and edge-case handling. A single borderline split near the root propagates to a completely different arrangement below it.
- **Slightly different padding amounts.** The exact pixel amounts subtracted for borders and inner gaps differ between the two renderers, changing the aspect ratio of the rectangle squarify receives at each level.
- **Rounding at different points.** D3 rounds tile coordinates after computing the full hierarchy; the Python code rounds after each recursive call and passes the rounded bounds to the next level, accumulating error differently.

### Interaction

| Action | Result |
|---|---|
| Click a file tile | Show info panel; load preview if Preview tab is open |
| Double-click a directory tile | Zoom into that directory |
| Click the canvas background | Clear selection |
| Right-click any tile | Open context menu (rename / delete if `--allow-write`) |
| Shift-click tiles | Multi-select |

### Keyboard shortcuts

All shortcuts are suppressed when an input field has focus. Press **Esc** to blur any input and return focus to the canvas.

| Key | Action |
|---|---|
| `j` | Move keyboard focus to next tile |
| `k` | Move keyboard focus to previous tile |
| `Enter` | Zoom into focused directory; preview focused file (if Preview tab open) |
| `Backspace` | Zoom out one level |
| `/` | Jump focus to the search field |
| `Esc` | Clear focus, close info panel, deselect all; blur any input |

The keyboard-focused tile is highlighted with an accent-coloured outline.

---

## Sidebar

The sidebar on the right has three tabs: **Settings**, **Metrics**, and **Preview**. Collapse or expand it with the `‹` / `›` toggle, or drag the resize handle.

### Settings

All settings apply instantly — there is no Apply button.

| Setting | Description |
|---|---|
| Dark mode | Toggle between dark and light theme |
| Depth | Maximum recursion depth (∞ button removes the limit) |
| Log scale | Compress the size range so small files remain visible (1 = off, 2–10) |
| Font size | Label font size inside tiles |
| Colormap | Color palette for file extensions |
| Code theme | Syntax highlighting theme for text file previews (10 paired dark/light options) |
| Canvas size | Fix the treemap to an explicit W × H (blank = auto) |
| Exclude | Glob patterns to hide, one per line |
| Include | Subtree paths to keep, one per line (all others hidden) |
| Highlight | `glob@color` rules, one per line — e.g. `**/*.py@orange` |

**Reset all** restores every setting to its startup default.

### Metrics

Displays aggregate statistics for the current tree: total size, file count, directory count, average file size, largest file, deepest path, and scan duration.

### Preview

Previews the last-clicked file. The panel stays empty until you click a tile — switching to this tab does **not** auto-load a preview; click a tile while the tab is open.

| File type | Preview |
|---|---|
| Images (PNG, JPG, GIF, SVG, WebP, BMP, ICO) | Inline `<img>` |
| Video (MP4, MOV, WebM) | Inline `<video controls>` player |
| PDF | Inline `<iframe>` with the browser's native PDF viewer |
| Text / code | Syntax-highlighted with highlight.js |
| Binary | Hex dump of the first 1 000 bytes |

For PNG, SVG, MP4, and MOV files that were created by dirplot, any embedded metadata (Date, Software, Command, Python, OS, URL) is shown in a table below the preview.

> **Note:** File preview is unavailable for remote sources (GitHub, S3, etc.) since the files are not accessible on the local filesystem.

---

## Live reload

The server watches the scanned directory for changes via a WebSocket connection. When files are added, removed, or modified the treemap refreshes automatically. Live reload is only active for local filesystem sources.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ nav:
- Home: index.md
- Installation: installation.md
- Examples: examples.md
- Web Interface: serve.md
- Remote Sources: remote-sources.md
- Archive Formats: archives.md
- CLI Reference: cli.md
Expand Down
8 changes: 6 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "dirplot"
version = "0.5.1"
version = "0.6.0"
description = "Nested treemap visualizations for directory trees and archives"
readme = "README.md"
license = {text = "MIT"}
Expand Down Expand Up @@ -40,6 +40,10 @@ dependencies = [
"click>=8.0",
"typer>=0.9",
"watchdog>=6.0.0",
"fastapi>=0.111",
"uvicorn[standard]>=0.29",
"jinja2>=3.1",
"python-multipart>=0.0.9",
]

[project.scripts]
Expand Down Expand Up @@ -114,7 +118,7 @@ python_version = "3.10"
strict = true

[[tool.mypy.overrides]]
module = ["squarify", "PIL.*", "paramiko.*", "boto3.*", "botocore.*", "py7zr.*", "rarfile", "drawsvg.*", "libarchive.*"]
module = ["squarify", "PIL.*", "paramiko.*", "boto3.*", "botocore.*", "py7zr.*", "rarfile", "drawsvg.*", "libarchive.*", "pillow_heif"]
ignore_missing_imports = true

# ---------------------------------------------------------------------------
Expand Down
124 changes: 124 additions & 0 deletions src/dirplot/commands/serve.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""The ``serve`` command: launch an interactive treemap website."""

from __future__ import annotations

import webbrowser

import typer

from dirplot.app import app
from dirplot.defaults import DEFAULT_COLORMAP

_DEFAULT_EXCLUDES: tuple[str, ...] = (
".git",
".venv",
"node_modules",
".yarn",
".pnpm-store",
"__pycache__",
"*.pyc",
".mypy_cache",
)

_SERVE_EPILOG = (
"[bold]Examples[/bold]\n\n"
" dirplot serve . [dim]# serve current directory[/dim]\n\n"
" dirplot serve src --port 9000 [dim]# custom port[/dim]\n\n"
" dirplot serve github://owner/repo --no-browser [dim]# read-only remote[/dim]\n\n"
" dirplot serve . --depth 4 [dim]# limit tree depth[/dim]\n\n"
"Requires [bold]dirplot[serve][/bold] extras:\n"
" pip install dirplot[serve]"
)


def _check_deps() -> None:
missing = []
for pkg in ("fastapi", "uvicorn", "jinja2"):
try:
__import__(pkg)
except ImportError:
missing.append(pkg)
if missing:
typer.echo(
f"[serve] Missing dependencies: {', '.join(missing)}\n"
"Install with: pip install 'dirplot[serve]'",
err=True,
)
raise typer.Exit(1)


@app.command(name="serve", epilog=_SERVE_EPILOG)
def serve_cmd(
root: str = typer.Argument(
default=".",
help=(
"Root path or source URL to serve. Accepts the same inputs as "
r"[bold]dirplot map[/bold]: local path, github://owner/repo, ssh://…, etc."
),
),
port: int = typer.Option(8765, "--port", "-p", help="HTTP port to bind.", show_default=True),
host: str = typer.Option("127.0.0.1", "--host", help="Bind address.", show_default=True),
no_browser: bool = typer.Option(
False, "--no-browser", help="Don't open the browser automatically."
),
depth: int | None = typer.Option(
None, "--depth", "-d", help="Maximum directory depth to scan.", metavar="N"
),
exclude: list[str] = typer.Option(
[],
"--exclude",
"-e",
help="Glob pattern(s) to exclude (repeatable).",
metavar="PATTERN",
),
colormap: str = typer.Option(
DEFAULT_COLORMAP, "--colormap", help="Matplotlib colormap name for file colors."
),
breadcrumbs: bool = typer.Option(
True,
"--breadcrumbs/--no-breadcrumbs",
help="Collapse single-subdirectory chains into breadcrumb labels.",
),
quiet: bool = typer.Option(False, "--quiet", "-q", help="Suppress uvicorn access logs."),
) -> None:
"""Launch an interactive treemap in the browser.

Opens a local web server and renders the directory tree as a zoomable,
navigable D3.js treemap. File operations (delete, move) are available
for local filesystem sources only.
"""
_check_deps()

import uvicorn

from dirplot.tree_json import is_readonly_source, resolve_root_path
from dirplot.web.server import ServeConfig, create_app

allow_write = not is_readonly_source(root)
root_path = resolve_root_path(root)

config = ServeConfig(
root=root,
root_path=root_path,
colormap=colormap,
depth=depth,
exclude=frozenset(exclude) if exclude else frozenset(_DEFAULT_EXCLUDES),
breadcrumbs=breadcrumbs,
allow_write=allow_write,
)

fastapi_app = create_app(config)

url = f"http://{host}:{port}/"
if not no_browser:
import threading

threading.Timer(0.8, webbrowser.open, args=[url]).start()

typer.echo(f"[serve] Serving {root!r} at {url} (Ctrl-C to stop)")
uvicorn.run(
fastapi_app,
host=host,
port=port,
log_level="warning" if quiet else "info",
)
2 changes: 2 additions & 0 deletions src/dirplot/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import dirplot.commands.metrics
import dirplot.commands.misc
import dirplot.commands.replay
import dirplot.commands.serve
import dirplot.commands.treemap
import dirplot.commands.vcs
import dirplot.commands.watch
Expand All @@ -21,6 +22,7 @@
"git",
"hg",
"watch",
"serve",
"replay",
"metrics",
"meta",
Expand Down
Loading
Loading