shrinkpath
Smart cross-platform path shortening for Rust.
/Users/john/Library/Application Support/Code/User/settings.json (62 chars)
↓ shrink_to(path, 35)
/Users/john/.../User/settings.json (35 chars)
Filename is never truncated. Username is preserved when possible. Everything else adapts to fit your target length.
Why shrinkpath?
- Target-length guarantee —
shrink_to(path, 30)always returns ≤ 30 chars (unless the filename itself is longer — filenames are sacred) - Identity-preserving — the username/profile segment is the last thing to go, not the first
- Cross-platform — handles
/home/...,~/...,C:\Users\...,\\server\share\...,.\...from any host OS - Zero dependencies — the library has no dependencies;
clapis only pulled in for the optional CLI - No filesystem access — works on path strings alone, so it runs in WASM, embedded, or anywhere (opt-in
fsfeature for filesystem-aware disambiguation)
Quick Start
# Cargo.toml
[]
= "0.1"
use ;
// Hybrid strategy: graduated shortening to fit a target length
let short = shrink_to;
assert!;
assert!;
// Fish strategy: every directory segment becomes its first character
let fish = shrink_fish;
assert_eq!;
Strategies
shrinkpath ships four strategies. Pick the one that fits your use case, or use Hybrid (the default) and let the algorithm decide.
Fish
Abbreviates every directory segment to its first character. Dot-prefixed
directories keep the dot: .config → .c.
/home/john/projects/rust/myapp/src/lib.rs → /h/j/p/r/m/s/lib.rs
C:\Users\Admin\AppData\Local\Temp\file.txt → C:\U\A\A\L\T\file.txt
~/projects/rust/file.rs → ~/p/r/file.rs
Fish produces the shortest possible result. Use it when every character counts (prompts, status bars) and the user can infer the full path from context.
Tuning knobs:
use ;
// Keep 2 chars per segment instead of 1 (like Starship's fish_style_pwd_dir_length)
let opts = new.strategy.dir_length;
let result = shrink;
assert_eq!;
// Keep the last N directory segments unabbreviated
let opts = new.strategy.full_length_dirs;
let result = shrink;
assert_eq!;
Ellipsis
Replaces middle segments with ..., keeping the identity head (username) and
the segments nearest the filename.
/home/john/projects/rust/myapp/src/lib.rs → /home/john/.../src/lib.rs
C:\Users\Admin\AppData\Local\Temp\file.txt → C:\Users\Admin\...\file.txt
Ellipsis is the most readable strategy. Use it when you have moderate space and want humans to immediately understand the path.
Hybrid (default)
A graduated four-phase approach that produces the best result for any target length:
Phase 1 — fish expendable segments only: /home/john/p/r/m/src/lib.rs
Phase 2 — fish context segments too: /h/john/p/r/m/s/lib.rs
Phase 3 — collapse abbreviated runs to ...: /h/john/.../s/lib.rs
Phase 4 — fish identity (last resort): /h/j/.../s/lib.rs
Each phase stops as soon as the result fits. If nothing fits, it falls back to
/.../<filename>, then the filename alone.
Unique
Disambiguates segments against each other within the same path. Each segment is abbreviated to the shortest prefix that distinguishes it from every other segment.
/home/documents/downloads/file.txt → /h/doc/dow/file.txt
/Users/Admin/AppData/Application/f → /U/Ad/AppD/Appl/f
When all first characters are unique, Unique behaves like Fish. When segments share prefixes, it uses the minimum characters needed. Identical segments are kept in full (they can't be disambiguated).
use shrink_unique;
let result = shrink_unique;
assert_eq!;
Features
Mapped Locations
Substitute known path prefixes before shortening. Useful for replacing home directories, project roots, or well-known paths with short aliases.
use ;
let opts = new
.map_location
.map_location;
// Longest match wins
let result = shrink;
assert!;
Anchor Segments
Mark directory names that should never be abbreviated, regardless of strategy.
use ;
let opts = new
.strategy
.anchor
.anchor;
let result = shrink;
// "myapp" and "src" kept full, everything else abbreviated
assert!;
assert!;
Segment Metadata
shrink_detailed() returns per-segment metadata for building colored prompts,
clickable breadcrumbs, or tooltip UIs.
use ;
let opts = new.strategy;
let result = shrink_detailed;
for seg in &result.segments
// seg.original always contains the full text for tooltips
Filesystem-Aware Features (opt-in)
Enable the fs feature for features that require filesystem access:
[]
= { = "0.1", = ["fs"] }
Git repo root detection — find the repository name for a file path:
use find_git_root;
if let Some = find_git_root
Filesystem-aware disambiguation — find the shortest unique prefix by checking against actual sibling directories (like Powerlevel10k):
use disambiguate_segment;
use Path;
// If /home contains "documents", "downloads", "desktop":
let short = disambiguate_segment;
// Returns "doc" (shortest prefix unique among siblings)
How It Works
Path Parsing
Every input path is parsed into three parts:
prefix segments (directories) filename
│ │ │
▼ ▼ ▼
/ home / john / projects / rust / src / lib.rs
~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~──────────── ~~~~~~
Prefix is the root: /, ~, C:\, \\server\share, ., or empty.
Filename is the last component. Segments are everything in between.
The path style (Unix or Windows) is auto-detected from the input string —
drive letters, UNC prefixes, and backslash heuristics are all recognized. You
can also force a style with ShrinkOptions::path_style().
Segment Priority
Each segment is classified by how important it is to keep:
| Priority | What | Example | Dropped |
|---|---|---|---|
| Sacred | Filename | lib.rs |
Never |
| Identity | Username / profile | john, Admin |
Last |
| Context | Home root, well-known dirs | home, Users |
Middle |
| Expendable | Everything else | projects, src |
First |
Identity is detected by recognizing the segment after a home root (home on
Unix, Users on Windows). The tilde prefix (~) encodes identity implicitly.
Fish Algorithm
for each directory segment (from left, skipping last full_length_dirs):
if segment is anchored:
keep full text
else if segment starts with '.':
keep '.' + first dir_length chars after dot (.config → .c)
else:
keep first dir_length chars (projects → p)
filename is never touched
Ellipsis Algorithm
1. Compute identity head = all segments up to and including the username
2. Base cost = prefix + separator + filename
3. Add head cost + ellipsis marker cost
4. With remaining budget, greedily add tail segments from right to left
5. If head doesn't fit, fall back to prefix + ... + filename
The greedy right-to-left fill keeps the segments closest to the filename, which provide the most context about what the file actually is.
Hybrid Algorithm
Phase 1: Fish all Expendable segments (respect anchors)
→ check if result fits target length
Phase 2: Fish all Context segments
→ check fit
Phase 3: Collapse consecutive abbreviated segments into ...
Try keeping 0-3 head + 0-3 tail segments, pick best fit
→ check fit
Phase 4: Fish all Identity segments (last resort)
→ try collapse again
Fallback: prefix + ... + filename
Last resort: filename only
The key insight is that each phase is strictly less destructive than the next. A 30-char budget on a moderately deep path might only need Phase 1. A 15-char budget on a deeply nested Windows path might need all four phases. The user sees the best possible result for their budget.
Unique Algorithm
1. Collect all segment texts
2. For each segment, find minimum prefix length L such that:
- segment[..L] differs from every other segment's first L chars
- For dot-prefixed: compare the part after the dot
3. Identical segments keep their full text (can't disambiguate)
4. Anchored segments are never abbreviated
CLI
# Basic usage
# Pipe paths from find, fd, rg, etc.
|
# Fish strategy
# Unique strategy
# JSON output with metadata
# {"original":"/home/john/projects/src/lib.rs","shortened":"/home/john/.../lib.rs",...}
# Custom ellipsis marker
Flags:
| Flag | Default | Description |
|---|---|---|
-m, --max-len |
40 |
Target maximum output length |
-s, --strategy |
hybrid |
fish, ellipsis, hybrid, or unique |
--style |
auto |
Force unix or windows separator style |
--ellipsis |
... |
Custom ellipsis marker string |
--json |
off | Output JSON with original, shortened, lengths, style |
API
// One-liner convenience functions
shrink_to // Hybrid strategy, target length
shrink_fish // Fish abbreviation, no length target
shrink_ellipsis // Ellipsis strategy, target length
shrink_unique // Unique disambiguation, no length target
// Full control
use ;
let opts = new
.strategy
.path_style
.ellipsis
.dir_length // chars per abbreviated segment
.full_length_dirs // keep last N dirs unabbreviated
.anchor // never abbreviate "src"
.map_location;
let result = shrink;
// With segment metadata
let detailed = shrink_detailed;
for seg in &detailed.segments
Platform Support
| Path format | Example | Detected as |
|---|---|---|
| Unix absolute | /home/john/file.rs |
Unix |
| Tilde home | ~/projects/file.rs |
Unix |
macOS /Users |
/Users/john/Documents/file.txt |
Unix |
| Windows drive | C:\Users\Admin\file.txt |
Windows |
| Windows UNC | \\server\share\dept\file.xlsx |
Windows |
| Dot-relative | .\src\main.rs |
Windows |
| Forward-slash drive | C:/Users/Admin/file.txt |
Windows |
| Relative (no prefix) | src/lib.rs |
Unix |
| Backslash heuristic | Users\Admin\file.txt |
Windows |
Detection is automatic. Use ShrinkOptions::path_style() to override.
Cargo Features
| Feature | Default | Description |
|---|---|---|
cli |
Yes | Builds the shrinkpath binary (pulls in clap) |
fs |
No | Enables filesystem-aware features (find_git_root, disambiguate_segment) |
# Library only, zero dependencies
= { = "0.1", = false }
# With filesystem features
= { = "0.1", = ["fs"] }
License
Licensed under either of Apache License, Version 2.0 or MIT License at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.