dirplot 0.3.2__tar.gz → 0.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dirplot-0.3.2 → dirplot-0.3.3}/CHANGELOG.md +14 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/PKG-INFO +7 -1
- {dirplot-0.3.2 → dirplot-0.3.3}/README.md +6 -0
- dirplot-0.3.3/breadcrumbs_test.png +0 -0
- dirplot-0.3.3/docs/docker.png +0 -0
- dirplot-0.3.3/docs/fastapi.png +0 -0
- dirplot-0.3.3/docs/flask.png +0 -0
- dirplot-0.3.3/docs/k8s.png +0 -0
- dirplot-0.3.3/docs/pypy.png +0 -0
- dirplot-0.3.3/docs/python.png +0 -0
- dirplot-0.3.3/docs/s3.png +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/pyproject.toml +1 -1
- {dirplot-0.3.2 → dirplot-0.3.3}/src/dirplot/main.py +76 -43
- {dirplot-0.3.2 → dirplot-0.3.3}/src/dirplot/render.py +43 -10
- {dirplot-0.3.2 → dirplot-0.3.3}/src/dirplot/scanner.py +36 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/src/dirplot/svg_render.py +26 -3
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/test_cli.py +21 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/test_scanner.py +78 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/uv.lock +1 -1
- dirplot-0.3.2/docs/docker.png +0 -0
- dirplot-0.3.2/docs/fastapi.png +0 -0
- dirplot-0.3.2/docs/flask.png +0 -0
- dirplot-0.3.2/docs/k8s.png +0 -0
- dirplot-0.3.2/docs/pypy.png +0 -0
- dirplot-0.3.2/docs/python.png +0 -0
- dirplot-0.3.2/docs/s3.png +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/.claude/settings.local.json +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/.dockerignore +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/.github/dependabot.yml +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/.github/workflows/ci.yml +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/.github/workflows/publish.yml +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/.gitignore +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/.ipynb_checkpoints/Untitled-checkpoint.ipynb +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/.pre-commit-config.yaml +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/.python-version +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/CONTRIBUTING.md +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/Dockerfile +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/LICENSE +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/Makefile +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/SECURITY.md +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/TASKS.md~ +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/alpine-pod.png +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/aws-lambda.png +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/demo.cast +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/demo.yaml +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/demo.yaml~ +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/demo1.cast +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/docs/ARCHIVES.md +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/docs/EXAMPLES.md +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/docs/SSH_DESIGN.md +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/docs/dirplot.png +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/foo.svg +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/gen_asciicast.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/misc/scan/FILE-SCANNING.md +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/misc/scan/compare.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/misc/scan/scan_async.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/misc/scan/scan_dir.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/misc/scan/scan_rust.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/misc/scan/scan_threaded.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/misc/scan/scan_walk.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/misc/watch/foo.txt +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/misc/watch/watch_dir.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/misc/watch/wordcloud1.png +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/misc-watched.png +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/openclaw.png +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/pg.png +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/scripts/animate_demo.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/scripts/make_docs_images.sh +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/scripts/make_fixtures.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/scripts/open_terminals.sh +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/src/dirplot/__init__.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/src/dirplot/__main__.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/src/dirplot/archives.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/src/dirplot/colors.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/src/dirplot/display.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/src/dirplot/docker.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/src/dirplot/fonts/JetBrainsMono-Bold.ttf +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/src/dirplot/fonts/JetBrainsMono-BoldItalic.ttf +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/src/dirplot/fonts/JetBrainsMono-Italic.ttf +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/src/dirplot/fonts/JetBrainsMono-Regular.ttf +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/src/dirplot/fonts/OFL.txt +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/src/dirplot/github.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/src/dirplot/k8s.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/src/dirplot/py.typed +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/src/dirplot/s3.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/src/dirplot/ssh.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/src/dirplot/terminal.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/src/dirplot/watch.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/src-portrait.png +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/src.pdf +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/src.png +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/src.svg +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/test-animation.png +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/__init__.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/animation/README.md +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/animation/changelog.txt +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/animation/config.toml +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/animation/data/records.csv +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/animation/data/schema_v1.json +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/animation/src/lib/parser.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/animation/src/main.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/conftest.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/example/bar/bar.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/example/bar/baz.json +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/example/foo/foo.md +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.7z +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.a +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.aab +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.apk +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.cpio +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.ear +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.epub +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.ipa +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.iso +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.jar +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.nupkg +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.rar +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.tar +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.tar.bz2 +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.tar.gz +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.tar.xz +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.tar.zst +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.tbz2 +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.tgz +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.txz +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.tzst +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.vsix +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.war +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.whl +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.xar +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.xpi +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/fixtures/sample.zip +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/test_archives.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/test_colors.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/test_display.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/test_docker.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/test_drawing.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/test_github.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/test_k8s.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/test_render.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/test_s3.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/test_ssh.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/test_svg_render.py +0 -0
- {dirplot-0.3.2 → dirplot-0.3.3}/tests/test_terminal.py +0 -0
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.3] - 2026-03-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Breadcrumbs mode** (`--breadcrumbs/--no-breadcrumbs`, `-b/-B`, on by default): directories
|
|
15
|
+
that form a single-child chain (one subdirectory, no files) are collapsed into a single tile
|
|
16
|
+
whose header shows the full path separated by ` / ` (e.g. `src / dirplot / fonts`). When the
|
|
17
|
+
label is too wide, middle segments are replaced with `…` (`src / … / fonts`). The root tile
|
|
18
|
+
is never collapsed. Disable with `-B` or `--no-breadcrumbs`.
|
|
19
|
+
- **Tree depth in root label**: the root tile header now includes `depth: N` alongside the
|
|
20
|
+
file, directory, and size summary (e.g. `myproject — 124 files, 18 dirs, 4.0 MB (…), depth: 6`).
|
|
21
|
+
The depth reflects the original tree structure and is invariant to whether breadcrumbs mode
|
|
22
|
+
is active.
|
|
23
|
+
|
|
10
24
|
## [0.3.2] - 2026-03-13
|
|
11
25
|
|
|
12
26
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dirplot
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Static treemap bitmaps for directory trees and archives, displayed as inline terminal images
|
|
5
5
|
Project-URL: Repository, https://github.com/deeplook/dirplot
|
|
6
6
|
License: MIT
|
|
@@ -78,6 +78,8 @@ Description-Content-Type: text/markdown
|
|
|
78
78
|
- Works on macOS, Linux, and Windows; WSL2 fully supported.
|
|
79
79
|
- Scan remote hosts over SSH (`pip install "dirplot[ssh]"`), AWS S3 buckets (`pip install "dirplot[s3]"`), any public/private GitHub repository (including specific branch, tag, commit SHA, or subdirectory), **running Docker containers**, or **Kubernetes pods** — all without extra dependencies beyond the respective CLI/SDK. See [EXAMPLES.md](docs/EXAMPLES.md).
|
|
80
80
|
- Optional **file-count legend** (`--legend`) — a corner overlay listing the top extensions by number of files, with coloured swatches and counts, automatically sized to fit the image.
|
|
81
|
+
- **Breadcrumbs mode** (on by default) — single-child directory chains are collapsed into one tile with a `foo / bar / baz` header label. Middle segments are replaced with `…` when the tile is too narrow. Disable with `-B` / `--no-breadcrumbs`.
|
|
82
|
+
- **Tree depth** shown in the root tile header alongside the file count and total size (e.g. `myproject — 124 files, 18 dirs, 4.0 MB (…), depth: 6`).
|
|
81
83
|
- **Wide archive support** — reads zip, tar (gz/bz2/xz/zst), 7z, rar, and via libarchive: iso, cpio, rpm, cab, lha/lzh, xar/pkg, a/ar, and all ZIP-based formats (jar, whl, apk, nupkg, vsix, ipa, …) as virtual directory trees without unpacking. Encrypted archives are handled gracefully: metadata-only reads work without a password for most formats; a password can be supplied with `--password` or entered interactively when needed.
|
|
82
84
|
|
|
83
85
|
## How It Works
|
|
@@ -158,6 +160,9 @@ dirplot map . --legend
|
|
|
158
160
|
# Show a file-count legend limited to 10 entries
|
|
159
161
|
dirplot map . --legend 10
|
|
160
162
|
|
|
163
|
+
# Disable breadcrumbs (show full nested hierarchy instead of collapsed chains)
|
|
164
|
+
dirplot map . -B
|
|
165
|
+
|
|
161
166
|
# Save as an interactive SVG (hover highlight + floating tooltip)
|
|
162
167
|
dirplot map . --output treemap.svg --no-show
|
|
163
168
|
|
|
@@ -182,6 +187,7 @@ dirplot map . --format svg --output treemap.svg --no-show
|
|
|
182
187
|
| `--header/--no-header` | | `--header` | Print info lines before rendering |
|
|
183
188
|
| `--cushion/--no-cushion` | | `--cushion` | Apply van Wijk cushion shading for a raised 3-D look |
|
|
184
189
|
| `--log/--no-log` | | `--no-log` | Use log of file sizes for layout, making small files more visible |
|
|
190
|
+
| `--breadcrumbs/--no-breadcrumbs` | `-b`/`-B` | `--breadcrumbs` | Collapse single-child directory chains into `foo / bar / baz` labels |
|
|
185
191
|
| `--password` | | — | Password for encrypted archives; prompted interactively if not supplied and needed |
|
|
186
192
|
| `--github-token` | | `$GITHUB_TOKEN` | GitHub personal access token for private repos or higher rate limits |
|
|
187
193
|
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
- Works on macOS, Linux, and Windows; WSL2 fully supported.
|
|
29
29
|
- Scan remote hosts over SSH (`pip install "dirplot[ssh]"`), AWS S3 buckets (`pip install "dirplot[s3]"`), any public/private GitHub repository (including specific branch, tag, commit SHA, or subdirectory), **running Docker containers**, or **Kubernetes pods** — all without extra dependencies beyond the respective CLI/SDK. See [EXAMPLES.md](docs/EXAMPLES.md).
|
|
30
30
|
- Optional **file-count legend** (`--legend`) — a corner overlay listing the top extensions by number of files, with coloured swatches and counts, automatically sized to fit the image.
|
|
31
|
+
- **Breadcrumbs mode** (on by default) — single-child directory chains are collapsed into one tile with a `foo / bar / baz` header label. Middle segments are replaced with `…` when the tile is too narrow. Disable with `-B` / `--no-breadcrumbs`.
|
|
32
|
+
- **Tree depth** shown in the root tile header alongside the file count and total size (e.g. `myproject — 124 files, 18 dirs, 4.0 MB (…), depth: 6`).
|
|
31
33
|
- **Wide archive support** — reads zip, tar (gz/bz2/xz/zst), 7z, rar, and via libarchive: iso, cpio, rpm, cab, lha/lzh, xar/pkg, a/ar, and all ZIP-based formats (jar, whl, apk, nupkg, vsix, ipa, …) as virtual directory trees without unpacking. Encrypted archives are handled gracefully: metadata-only reads work without a password for most formats; a password can be supplied with `--password` or entered interactively when needed.
|
|
32
34
|
|
|
33
35
|
## How It Works
|
|
@@ -108,6 +110,9 @@ dirplot map . --legend
|
|
|
108
110
|
# Show a file-count legend limited to 10 entries
|
|
109
111
|
dirplot map . --legend 10
|
|
110
112
|
|
|
113
|
+
# Disable breadcrumbs (show full nested hierarchy instead of collapsed chains)
|
|
114
|
+
dirplot map . -B
|
|
115
|
+
|
|
111
116
|
# Save as an interactive SVG (hover highlight + floating tooltip)
|
|
112
117
|
dirplot map . --output treemap.svg --no-show
|
|
113
118
|
|
|
@@ -132,6 +137,7 @@ dirplot map . --format svg --output treemap.svg --no-show
|
|
|
132
137
|
| `--header/--no-header` | | `--header` | Print info lines before rendering |
|
|
133
138
|
| `--cushion/--no-cushion` | | `--cushion` | Apply van Wijk cushion shading for a raised 3-D look |
|
|
134
139
|
| `--log/--no-log` | | `--no-log` | Use log of file sizes for layout, making small files more visible |
|
|
140
|
+
| `--breadcrumbs/--no-breadcrumbs` | `-b`/`-B` | `--breadcrumbs` | Collapse single-child directory chains into `foo / bar / baz` labels |
|
|
135
141
|
| `--password` | | — | Password for encrypted archives; prompted interactively if not supplied and needed |
|
|
136
142
|
| `--github-token` | | `$GITHUB_TOKEN` | GitHub personal access token for private repos or higher rate limits |
|
|
137
143
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dirplot"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.3"
|
|
8
8
|
description = "Static treemap bitmaps for directory trees and archives, displayed as inline terminal images"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -19,10 +19,12 @@ from dirplot.render import create_treemap
|
|
|
19
19
|
from dirplot.s3 import build_tree_s3, is_s3_path, make_s3_client, parse_s3_path
|
|
20
20
|
from dirplot.scanner import (
|
|
21
21
|
Node,
|
|
22
|
+
apply_breadcrumbs,
|
|
22
23
|
apply_log_sizes,
|
|
23
24
|
build_tree,
|
|
24
25
|
build_tree_multi,
|
|
25
26
|
collect_extensions,
|
|
27
|
+
max_depth,
|
|
26
28
|
prune_to_subtrees,
|
|
27
29
|
)
|
|
28
30
|
from dirplot.ssh import build_tree_ssh, connect, is_ssh_path, parse_ssh_path
|
|
@@ -175,50 +177,65 @@ def watch_cmd(
|
|
|
175
177
|
|
|
176
178
|
@app.command(name="read-meta")
|
|
177
179
|
def read_meta(
|
|
178
|
-
|
|
180
|
+
files: list[Path] = typer.Argument(
|
|
181
|
+
..., help="PNG or SVG file(s) to read dirplot metadata from"
|
|
182
|
+
),
|
|
179
183
|
) -> None:
|
|
180
|
-
"""Read dirplot metadata embedded in
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
184
|
+
"""Read dirplot metadata embedded in one or more PNG or SVG files."""
|
|
185
|
+
any_error = False
|
|
186
|
+
|
|
187
|
+
for file in files:
|
|
188
|
+
if len(files) > 1:
|
|
189
|
+
typer.echo(f"==> {file} <==")
|
|
190
|
+
|
|
191
|
+
if not file.exists():
|
|
192
|
+
typer.echo(f"Error: file not found: {file}", err=True)
|
|
193
|
+
any_error = True
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
suffix = file.suffix.lower()
|
|
197
|
+
|
|
198
|
+
if suffix == ".png":
|
|
199
|
+
from PIL import Image
|
|
200
|
+
|
|
201
|
+
img = Image.open(file)
|
|
202
|
+
info = img.info
|
|
203
|
+
meta_keys = {"Date", "Software", "URL", "Python", "OS", "Command"}
|
|
204
|
+
found = {k: v for k, v in info.items() if k in meta_keys}
|
|
205
|
+
if not found:
|
|
206
|
+
typer.echo("No dirplot metadata found in PNG.", err=True)
|
|
207
|
+
any_error = True
|
|
208
|
+
continue
|
|
209
|
+
for k, v in found.items():
|
|
210
|
+
typer.echo(f"{k}: {v}")
|
|
211
|
+
|
|
212
|
+
elif suffix == ".svg":
|
|
213
|
+
content = file.read_text(encoding="utf-8")
|
|
214
|
+
try:
|
|
215
|
+
root = ET.fromstring(content)
|
|
216
|
+
except ET.ParseError as exc:
|
|
217
|
+
typer.echo(f"Error parsing SVG: {exc}", err=True)
|
|
218
|
+
any_error = True
|
|
219
|
+
continue
|
|
220
|
+
svg_meta: dict[str, str] = {}
|
|
221
|
+
for desc in root.iter("{http://www.w3.org/1999/02/22-rdf-syntax-ns#}Description"):
|
|
222
|
+
for child in desc:
|
|
223
|
+
local = child.tag.split("}")[-1] if "}" in child.tag else child.tag
|
|
224
|
+
ns_uri = child.tag.split("}")[0].lstrip("{") if "}" in child.tag else ""
|
|
225
|
+
if ns_uri == "https://github.com/deeplook/dirplot#" and child.text:
|
|
226
|
+
svg_meta[local] = child.text
|
|
227
|
+
if not svg_meta:
|
|
228
|
+
typer.echo("No dirplot metadata found in SVG.", err=True)
|
|
229
|
+
any_error = True
|
|
230
|
+
continue
|
|
231
|
+
for k, v in svg_meta.items():
|
|
232
|
+
typer.echo(f"{k}: {v}")
|
|
199
233
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
root = ET.fromstring(content)
|
|
204
|
-
except ET.ParseError as exc:
|
|
205
|
-
typer.echo(f"Error parsing SVG: {exc}", err=True)
|
|
206
|
-
raise typer.Exit(1) from exc
|
|
207
|
-
svg_meta: dict[str, str] = {}
|
|
208
|
-
for desc in root.iter("{http://www.w3.org/1999/02/22-rdf-syntax-ns#}Description"):
|
|
209
|
-
for child in desc:
|
|
210
|
-
local = child.tag.split("}")[-1] if "}" in child.tag else child.tag
|
|
211
|
-
ns_uri = child.tag.split("}")[0].lstrip("{") if "}" in child.tag else ""
|
|
212
|
-
if ns_uri == "https://github.com/deeplook/dirplot#" and child.text:
|
|
213
|
-
svg_meta[local] = child.text
|
|
214
|
-
if not svg_meta:
|
|
215
|
-
typer.echo("No dirplot metadata found in SVG.", err=True)
|
|
216
|
-
raise typer.Exit(1)
|
|
217
|
-
for k, v in svg_meta.items():
|
|
218
|
-
typer.echo(f"{k}: {v}")
|
|
234
|
+
else:
|
|
235
|
+
typer.echo(f"Unsupported file type: {suffix!r}. Expected .png or .svg", err=True)
|
|
236
|
+
any_error = True
|
|
219
237
|
|
|
220
|
-
|
|
221
|
-
typer.echo(f"Unsupported file type: {suffix!r}. Expected .png or .svg", err=True)
|
|
238
|
+
if any_error:
|
|
222
239
|
raise typer.Exit(1)
|
|
223
240
|
|
|
224
241
|
|
|
@@ -331,6 +348,15 @@ def main(
|
|
|
331
348
|
"--password",
|
|
332
349
|
help="Password for encrypted archives. Prompted interactively if not supplied and needed.",
|
|
333
350
|
),
|
|
351
|
+
breadcrumbs: bool = typer.Option(
|
|
352
|
+
True,
|
|
353
|
+
"--breadcrumbs/--no-breadcrumbs",
|
|
354
|
+
"-b/-B",
|
|
355
|
+
help=(
|
|
356
|
+
"Collapse single-subdirectory chains into breadcrumb labels"
|
|
357
|
+
" (e.g. foo / bar / baz). Default: on."
|
|
358
|
+
),
|
|
359
|
+
),
|
|
334
360
|
) -> None:
|
|
335
361
|
"""Create a nested treemap bitmap for a directory tree."""
|
|
336
362
|
if not roots:
|
|
@@ -544,6 +570,11 @@ def main(
|
|
|
544
570
|
if subtrees:
|
|
545
571
|
root_node = prune_to_subtrees(root_node, set(subtrees))
|
|
546
572
|
|
|
573
|
+
tree_depth = max_depth(root_node)
|
|
574
|
+
|
|
575
|
+
if breadcrumbs:
|
|
576
|
+
root_node = apply_breadcrumbs(root_node)
|
|
577
|
+
|
|
547
578
|
t_scan = time.monotonic() - t_scan_start
|
|
548
579
|
if log:
|
|
549
580
|
apply_log_sizes(root_node)
|
|
@@ -587,10 +618,12 @@ def main(
|
|
|
587
618
|
t_render_start = time.monotonic()
|
|
588
619
|
if use_svg:
|
|
589
620
|
buf = create_treemap_svg(
|
|
590
|
-
root_node, width_px, height_px, font_size, colormap, legend, cushion
|
|
621
|
+
root_node, width_px, height_px, font_size, colormap, legend, cushion, tree_depth
|
|
591
622
|
)
|
|
592
623
|
else:
|
|
593
|
-
buf = create_treemap(
|
|
624
|
+
buf = create_treemap(
|
|
625
|
+
root_node, width_px, height_px, font_size, colormap, legend, cushion, tree_depth
|
|
626
|
+
)
|
|
594
627
|
t_render = time.monotonic() - t_render_start
|
|
595
628
|
|
|
596
629
|
if output is not None:
|
|
@@ -14,7 +14,7 @@ import squarify
|
|
|
14
14
|
from PIL import Image, ImageDraw, ImageFont, PngImagePlugin
|
|
15
15
|
|
|
16
16
|
from dirplot.colors import RGBAColor, assign_colors
|
|
17
|
-
from dirplot.scanner import Node, collect_extensions, count_nodes
|
|
17
|
+
from dirplot.scanner import Node, collect_extensions, count_nodes, max_depth
|
|
18
18
|
|
|
19
19
|
DIRPLOT_URL = "https://github.com/deeplook/dirplot"
|
|
20
20
|
|
|
@@ -160,6 +160,26 @@ def _truncate(
|
|
|
160
160
|
return name[:lo] + ellipsis
|
|
161
161
|
|
|
162
162
|
|
|
163
|
+
def _truncate_breadcrumb(
|
|
164
|
+
name: str, draw: ImageDraw.ImageDraw, font: ImageFont.FreeTypeFont, max_w: int
|
|
165
|
+
) -> str:
|
|
166
|
+
"""Truncate a breadcrumb label (`` / ``-separated parts) to fit *max_w* pixels.
|
|
167
|
+
|
|
168
|
+
Tries the full label first, then collapses middle segments to ``…``, and
|
|
169
|
+
finally falls back to ``_truncate`` for plain names or when even the
|
|
170
|
+
``first / … / last`` form is too long.
|
|
171
|
+
"""
|
|
172
|
+
parts = name.split(" / ")
|
|
173
|
+
if len(parts) <= 1:
|
|
174
|
+
return _truncate(name, draw, font, max_w)
|
|
175
|
+
if _text_w(draw, name, font) <= max_w:
|
|
176
|
+
return name
|
|
177
|
+
candidate = parts[0] + " / … / " + parts[-1]
|
|
178
|
+
if _text_w(draw, candidate, font) <= max_w:
|
|
179
|
+
return candidate
|
|
180
|
+
return _truncate(candidate, draw, font, max_w)
|
|
181
|
+
|
|
182
|
+
|
|
163
183
|
def _apply_cushion(img: Image.Image, x: int, y: int, w: int, h: int) -> None:
|
|
164
184
|
"""Apply van Wijk-style quadratic cushion shading to a tile in-place."""
|
|
165
185
|
if w < 4 or h < 4:
|
|
@@ -233,30 +253,39 @@ def draw_node(
|
|
|
233
253
|
draw.rectangle([x, y, x + w - 1, y + h - 1], outline=dark)
|
|
234
254
|
# Adaptive label: largest font that fits the tile without overflow
|
|
235
255
|
if w > 20 and h > 10:
|
|
256
|
+
# Try horizontal first
|
|
257
|
+
ffont_h, label_h = _fit_font(node.name, draw, font_size + 2, w - 4, h - 4)
|
|
258
|
+
# For tall narrow tiles, also try vertical; prefer whichever wraps less
|
|
259
|
+
use_vertical = False
|
|
236
260
|
if h >= w * 2 and img is not None:
|
|
261
|
+
ffont_v, label_v = _fit_font(node.name, draw, font_size + 2, h - 4, w - 4)
|
|
262
|
+
if label_v:
|
|
263
|
+
h_lines = label_h.count("\n") + 1 if label_h else 999
|
|
264
|
+
v_lines = label_v.count("\n") + 1
|
|
265
|
+
if v_lines < h_lines or (v_lines == h_lines and ffont_v.size > ffont_h.size):
|
|
266
|
+
use_vertical = True
|
|
267
|
+
if use_vertical:
|
|
237
268
|
# Tall, narrow tile — rotate label 90° CCW so it runs along the height
|
|
238
|
-
# available text-run = h-4, constraining dim = w-4
|
|
239
|
-
ffont, label = _fit_font(node.name, draw, font_size + 2, h - 4, w - 4)
|
|
240
269
|
tmp = Image.new("RGBA", (h, w), (0, 0, 0, 0))
|
|
241
270
|
ImageDraw.Draw(tmp).text(
|
|
242
271
|
(h // 2, w // 2),
|
|
243
|
-
|
|
272
|
+
label_v,
|
|
244
273
|
fill=_label_color(rgb),
|
|
245
|
-
font=
|
|
274
|
+
font=ffont_v,
|
|
246
275
|
anchor="mm",
|
|
247
276
|
align="center",
|
|
248
277
|
spacing=0,
|
|
249
278
|
)
|
|
250
279
|
rotated = tmp.rotate(90, expand=True)
|
|
280
|
+
assert img is not None
|
|
251
281
|
img.paste(rotated, (x, y), mask=rotated)
|
|
252
282
|
else:
|
|
253
283
|
# Horizontal label: available text-run = w-4, constraining dim = h-4
|
|
254
|
-
ffont, label = _fit_font(node.name, draw, font_size + 2, w - 4, h - 4)
|
|
255
284
|
draw.text(
|
|
256
285
|
(x + w // 2, y + h // 2),
|
|
257
|
-
|
|
286
|
+
label_h,
|
|
258
287
|
fill=_label_color(rgb),
|
|
259
|
-
font=
|
|
288
|
+
font=ffont_h,
|
|
260
289
|
anchor="mm",
|
|
261
290
|
align="center",
|
|
262
291
|
spacing=0,
|
|
@@ -271,7 +300,9 @@ def draw_node(
|
|
|
271
300
|
# Header label — height driven by the font size
|
|
272
301
|
header_h = font.size + 4
|
|
273
302
|
if h > 2 + header_h:
|
|
274
|
-
label =
|
|
303
|
+
label = _truncate_breadcrumb(
|
|
304
|
+
root_label if root_label is not None else node.name, draw, font, w - 8
|
|
305
|
+
)
|
|
275
306
|
draw.text(
|
|
276
307
|
(x + w // 2, y + 2 + header_h // 2),
|
|
277
308
|
label,
|
|
@@ -436,6 +467,7 @@ def create_treemap(
|
|
|
436
467
|
colormap: str = "tab20",
|
|
437
468
|
legend: int | None = None,
|
|
438
469
|
cushion: bool = True,
|
|
470
|
+
tree_depth: int | None = None,
|
|
439
471
|
) -> io.BytesIO:
|
|
440
472
|
"""Render a nested squarified treemap and return it as a PNG in a BytesIO buffer.
|
|
441
473
|
|
|
@@ -459,9 +491,10 @@ def create_treemap(
|
|
|
459
491
|
|
|
460
492
|
n_files, n_dirs = count_nodes(root_node)
|
|
461
493
|
total_bytes = root_node.original_size if root_node.original_size > 0 else root_node.size
|
|
494
|
+
depth = tree_depth if tree_depth is not None else max_depth(root_node)
|
|
462
495
|
root_label = (
|
|
463
496
|
f"{root_node.name} \u2014 {n_files:,} files, {n_dirs:,} dirs,"
|
|
464
|
-
f" {_human_bytes(total_bytes)} ({total_bytes:,} bytes)"
|
|
497
|
+
f" {_human_bytes(total_bytes)} ({total_bytes:,} bytes), depth: {depth}"
|
|
465
498
|
)
|
|
466
499
|
draw_node(
|
|
467
500
|
idraw,
|
|
@@ -182,6 +182,42 @@ def count_nodes(node: Node) -> tuple[int, int]:
|
|
|
182
182
|
return files, dirs
|
|
183
183
|
|
|
184
184
|
|
|
185
|
+
def _apply_breadcrumbs_recursive(node: Node) -> Node:
|
|
186
|
+
"""Recursively collapse single-subdirectory chains (internal helper)."""
|
|
187
|
+
node.children = [_apply_breadcrumbs_recursive(c) for c in node.children]
|
|
188
|
+
dir_children = [c for c in node.children if c.is_dir]
|
|
189
|
+
file_children = [c for c in node.children if not c.is_dir]
|
|
190
|
+
if node.is_dir and len(dir_children) == 1 and len(file_children) == 0:
|
|
191
|
+
child = dir_children[0]
|
|
192
|
+
node.name = f"{node.name} / {child.name}"
|
|
193
|
+
node.children = child.children
|
|
194
|
+
return node
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def apply_breadcrumbs(node: Node) -> Node:
|
|
198
|
+
"""Collapse single-subdirectory chains into one node with a combined name.
|
|
199
|
+
|
|
200
|
+
A directory that has exactly one directory child and no file children is
|
|
201
|
+
merged with that child: the names are joined with `` / `` and the child's
|
|
202
|
+
children become this node's children. The process is bottom-up so chains
|
|
203
|
+
of any length accumulate naturally.
|
|
204
|
+
|
|
205
|
+
The root node itself is never collapsed — only its descendants are.
|
|
206
|
+
"""
|
|
207
|
+
node.children = [_apply_breadcrumbs_recursive(c) for c in node.children]
|
|
208
|
+
return node
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def max_depth(node: Node) -> int:
|
|
212
|
+
"""Return the maximum depth of the tree rooted at *node*.
|
|
213
|
+
|
|
214
|
+
A leaf node (no children) has depth 0.
|
|
215
|
+
"""
|
|
216
|
+
if not node.children:
|
|
217
|
+
return 0
|
|
218
|
+
return 1 + max(max_depth(c) for c in node.children)
|
|
219
|
+
|
|
220
|
+
|
|
185
221
|
def collect_extensions(node: Node) -> list[str]:
|
|
186
222
|
"""Return a flat list of file extensions under *node*."""
|
|
187
223
|
if not node.is_dir:
|
|
@@ -9,7 +9,7 @@ import squarify
|
|
|
9
9
|
|
|
10
10
|
from dirplot.colors import RGBAColor, assign_colors
|
|
11
11
|
from dirplot.render import _human_bytes, build_metadata
|
|
12
|
-
from dirplot.scanner import Node, collect_extensions, count_nodes
|
|
12
|
+
from dirplot.scanner import Node, collect_extensions, count_nodes, max_depth
|
|
13
13
|
|
|
14
14
|
_CHAR_ASPECT = 0.6 # approximate width/height ratio for monospace font
|
|
15
15
|
_FONT_FAMILY = "JetBrains Mono, Consolas, monospace"
|
|
@@ -247,6 +247,25 @@ def _truncate(name: str, font_size: int, max_w: float) -> str:
|
|
|
247
247
|
return name[: max(0, max_chars - 1)] + "\u2026"
|
|
248
248
|
|
|
249
249
|
|
|
250
|
+
def _truncate_breadcrumb_svg(name: str, font_size: int, max_w: float) -> str:
|
|
251
|
+
"""Truncate a breadcrumb label (`` / ``-separated parts) to fit *max_w*.
|
|
252
|
+
|
|
253
|
+
Tries the full label first, then collapses middle segments to ``…``, and
|
|
254
|
+
finally falls back to ``_truncate`` for plain names or when even the
|
|
255
|
+
``first / … / last`` form is too long.
|
|
256
|
+
"""
|
|
257
|
+
char_w = font_size * _CHAR_ASPECT
|
|
258
|
+
parts = name.split(" / ")
|
|
259
|
+
if len(parts) <= 1:
|
|
260
|
+
return _truncate(name, font_size, max_w)
|
|
261
|
+
if len(name) * char_w <= max_w:
|
|
262
|
+
return name
|
|
263
|
+
candidate = parts[0] + " / \u2026 / " + parts[-1]
|
|
264
|
+
if len(candidate) * char_w <= max_w:
|
|
265
|
+
return candidate
|
|
266
|
+
return _truncate(candidate, font_size, max_w)
|
|
267
|
+
|
|
268
|
+
|
|
250
269
|
# ---------------------------------------------------------------------------
|
|
251
270
|
# Recursive draw
|
|
252
271
|
# ---------------------------------------------------------------------------
|
|
@@ -367,7 +386,9 @@ def _draw_node_svg(
|
|
|
367
386
|
)
|
|
368
387
|
d.append(hdr)
|
|
369
388
|
|
|
370
|
-
label =
|
|
389
|
+
label = _truncate_breadcrumb_svg(
|
|
390
|
+
root_label if root_label is not None else node.name, font_size, w - 8
|
|
391
|
+
)
|
|
371
392
|
hclip = drawsvg.ClipPath()
|
|
372
393
|
hclip.append(drawsvg.Rectangle(x + 2, y + 2, w - 4, header_h))
|
|
373
394
|
d.append(hclip)
|
|
@@ -535,6 +556,7 @@ def create_treemap_svg(
|
|
|
535
556
|
colormap: str = "tab20",
|
|
536
557
|
legend: int | None = None,
|
|
537
558
|
cushion: bool = True,
|
|
559
|
+
tree_depth: int | None = None,
|
|
538
560
|
) -> io.BytesIO:
|
|
539
561
|
"""Render a nested squarified treemap and return it as SVG in a BytesIO buffer.
|
|
540
562
|
|
|
@@ -598,9 +620,10 @@ def create_treemap_svg(
|
|
|
598
620
|
# 4. Treemap tiles
|
|
599
621
|
n_files, n_dirs = count_nodes(root_node)
|
|
600
622
|
total_bytes = root_node.original_size if root_node.original_size > 0 else root_node.size
|
|
623
|
+
depth = tree_depth if tree_depth is not None else max_depth(root_node)
|
|
601
624
|
root_label = (
|
|
602
625
|
f"{root_node.name} \u2014 {n_files:,} files, {n_dirs:,} dirs,"
|
|
603
|
-
f" {_human_bytes(total_bytes)} ({total_bytes:,} bytes)"
|
|
626
|
+
f" {_human_bytes(total_bytes)} ({total_bytes:,} bytes), depth: {depth}"
|
|
604
627
|
)
|
|
605
628
|
_draw_node_svg(
|
|
606
629
|
d,
|
|
@@ -163,6 +163,27 @@ def test_read_meta_svg_no_metadata(tmp_path: Path) -> None:
|
|
|
163
163
|
assert "No dirplot metadata" in result.output
|
|
164
164
|
|
|
165
165
|
|
|
166
|
+
def test_read_meta_multiple_files(sample_tree: Path, tmp_path: Path) -> None:
|
|
167
|
+
out1 = tmp_path / "out1.png"
|
|
168
|
+
out2 = tmp_path / "out2.png"
|
|
169
|
+
runner.invoke(app, ["map", str(sample_tree), "--no-show", "--output", str(out1)])
|
|
170
|
+
runner.invoke(app, ["map", str(sample_tree), "--no-show", "--output", str(out2)])
|
|
171
|
+
result = runner.invoke(app, ["read-meta", str(out1), str(out2)])
|
|
172
|
+
assert result.exit_code == 0
|
|
173
|
+
assert f"==> {out1} <==" in result.output
|
|
174
|
+
assert f"==> {out2} <==" in result.output
|
|
175
|
+
assert result.output.count("Date:") == 2
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_read_meta_multiple_files_partial_error(sample_tree: Path, tmp_path: Path) -> None:
|
|
179
|
+
out1 = tmp_path / "out1.png"
|
|
180
|
+
runner.invoke(app, ["map", str(sample_tree), "--no-show", "--output", str(out1)])
|
|
181
|
+
result = runner.invoke(app, ["read-meta", str(out1), "/nonexistent/file.png"])
|
|
182
|
+
assert result.exit_code == 1
|
|
183
|
+
assert "Date:" in result.output
|
|
184
|
+
assert "file not found" in result.output
|
|
185
|
+
|
|
186
|
+
|
|
166
187
|
def test_main_module() -> None:
|
|
167
188
|
"""__main__.py delegates to app."""
|
|
168
189
|
from dirplot.__main__ import main
|
|
@@ -6,6 +6,7 @@ import pytest
|
|
|
6
6
|
|
|
7
7
|
from dirplot.scanner import (
|
|
8
8
|
Node,
|
|
9
|
+
apply_breadcrumbs,
|
|
9
10
|
build_tree,
|
|
10
11
|
build_tree_multi,
|
|
11
12
|
collect_extensions,
|
|
@@ -229,3 +230,80 @@ def test_build_tree_multi_single_delegates(tmp_path: Path) -> None:
|
|
|
229
230
|
root = build_tree_multi([tmp_path])
|
|
230
231
|
assert root.path == tmp_path
|
|
231
232
|
assert len(root.children) == 1
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ---------------------------------------------------------------------------
|
|
236
|
+
# apply_breadcrumbs
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _make_dir(name: str, children: list[Node] | None = None) -> Node:
|
|
241
|
+
return Node(name=name, path=Path(name), size=1, is_dir=True, children=children or [])
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _make_file(name: str) -> Node:
|
|
245
|
+
return Node(name=name, path=Path(name), size=1, is_dir=False, extension=".txt")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def test_breadcrumbs_collapses_chain() -> None:
|
|
249
|
+
# root → a → b → c → [file.txt]; root is never collapsed, but a/b/c merge
|
|
250
|
+
file_node = _make_file("file.txt")
|
|
251
|
+
c = _make_dir("c", [file_node])
|
|
252
|
+
b = _make_dir("b", [c])
|
|
253
|
+
a = _make_dir("a", [b])
|
|
254
|
+
root = _make_dir("root", [a])
|
|
255
|
+
|
|
256
|
+
result = apply_breadcrumbs(root)
|
|
257
|
+
|
|
258
|
+
assert result.name == "root" # root itself is never collapsed
|
|
259
|
+
assert len(result.children) == 1
|
|
260
|
+
merged = result.children[0]
|
|
261
|
+
assert merged.name == "a / b / c"
|
|
262
|
+
assert len(merged.children) == 1
|
|
263
|
+
assert merged.children[0].name == "file.txt"
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def test_breadcrumbs_no_collapse_with_files() -> None:
|
|
267
|
+
# root → a → [file.txt, subdir] — a has file child, must not collapse
|
|
268
|
+
file_node = _make_file("file.txt")
|
|
269
|
+
subdir = _make_dir("subdir", [_make_file("inner.txt")])
|
|
270
|
+
a = _make_dir("a", [file_node, subdir])
|
|
271
|
+
root = _make_dir("root", [a])
|
|
272
|
+
|
|
273
|
+
result = apply_breadcrumbs(root)
|
|
274
|
+
|
|
275
|
+
assert result.name == "root"
|
|
276
|
+
child = result.children[0]
|
|
277
|
+
assert child.name == "a"
|
|
278
|
+
assert {c.name for c in child.children} == {"file.txt", "subdir"}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def test_breadcrumbs_no_collapse_multi_children() -> None:
|
|
282
|
+
# root → a → [dir1, dir2] — a has two dir children, must not collapse
|
|
283
|
+
dir1 = _make_dir("dir1", [_make_file("x.txt")])
|
|
284
|
+
dir2 = _make_dir("dir2", [_make_file("y.txt")])
|
|
285
|
+
a = _make_dir("a", [dir1, dir2])
|
|
286
|
+
root = _make_dir("root", [a])
|
|
287
|
+
|
|
288
|
+
result = apply_breadcrumbs(root)
|
|
289
|
+
|
|
290
|
+
assert result.name == "root"
|
|
291
|
+
child = result.children[0]
|
|
292
|
+
assert child.name == "a"
|
|
293
|
+
assert {c.name for c in child.children} == {"dir1", "dir2"}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def test_breadcrumbs_partial_chain() -> None:
|
|
297
|
+
# root → a → b → [dir1, dir2] — b has two children, so a/b merges but stops there
|
|
298
|
+
dir1 = _make_dir("dir1", [_make_file("x.txt")])
|
|
299
|
+
dir2 = _make_dir("dir2", [_make_file("y.txt")])
|
|
300
|
+
b = _make_dir("b", [dir1, dir2])
|
|
301
|
+
a = _make_dir("a", [b])
|
|
302
|
+
root = _make_dir("root", [a])
|
|
303
|
+
|
|
304
|
+
result = apply_breadcrumbs(root)
|
|
305
|
+
|
|
306
|
+
assert result.name == "root"
|
|
307
|
+
child = result.children[0]
|
|
308
|
+
assert child.name == "a / b"
|
|
309
|
+
assert {c.name for c in child.children} == {"dir1", "dir2"}
|
dirplot-0.3.2/docs/docker.png
DELETED
|
Binary file
|
dirplot-0.3.2/docs/fastapi.png
DELETED
|
Binary file
|
dirplot-0.3.2/docs/flask.png
DELETED
|
Binary file
|
dirplot-0.3.2/docs/k8s.png
DELETED
|
Binary file
|
dirplot-0.3.2/docs/pypy.png
DELETED
|
Binary file
|
dirplot-0.3.2/docs/python.png
DELETED
|
Binary file
|
dirplot-0.3.2/docs/s3.png
DELETED
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|