feat(utils): add abbreviate_path for smart path/URL truncation
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
"""Path abbreviation utilities for display."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
def abbreviate_path(path: str, max_len: int = 40) -> str:
|
||||
"""Abbreviate a file path or URL, preserving basename and key directories.
|
||||
|
||||
Strategy:
|
||||
1. Return as-is if short enough
|
||||
2. Replace home directory with ~/
|
||||
3. From right, keep basename + parent dirs until budget exhausted
|
||||
4. Prefix with …/
|
||||
"""
|
||||
if not path:
|
||||
return path
|
||||
|
||||
# Handle URLs: preserve scheme://domain + filename
|
||||
if re.match(r"https?://", path):
|
||||
return _abbreviate_url(path, max_len)
|
||||
|
||||
# Normalize separators to /
|
||||
normalized = path.replace("\\", "/")
|
||||
|
||||
# Replace home directory
|
||||
home = os.path.expanduser("~").replace("\\", "/")
|
||||
if normalized.startswith(home + "/"):
|
||||
normalized = "~" + normalized[len(home):]
|
||||
elif normalized == home:
|
||||
normalized = "~"
|
||||
|
||||
# Return early only after normalization and home replacement
|
||||
if len(normalized) <= max_len:
|
||||
return normalized
|
||||
|
||||
# Split into segments
|
||||
parts = normalized.rstrip("/").split("/")
|
||||
if len(parts) <= 1:
|
||||
return normalized[:max_len - 1] + "\u2026"
|
||||
|
||||
# Always keep the basename
|
||||
basename = parts[-1]
|
||||
# Budget: max_len minus "…/" prefix (2 chars) minus "/" separator minus basename
|
||||
budget = max_len - len(basename) - 3 # -3 for "…/" + final "/"
|
||||
|
||||
# Walk backwards from parent, collecting segments
|
||||
kept: list[str] = []
|
||||
for seg in reversed(parts[:-1]):
|
||||
needed = len(seg) + 1 # segment + "/"
|
||||
if not kept and needed <= budget:
|
||||
kept.append(seg)
|
||||
budget -= needed
|
||||
elif kept:
|
||||
needed_with_sep = len(seg) + 1
|
||||
if needed_with_sep <= budget:
|
||||
kept.append(seg)
|
||||
budget -= needed_with_sep
|
||||
else:
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
kept.reverse()
|
||||
if kept:
|
||||
return "\u2026/" + "/".join(kept) + "/" + basename
|
||||
return "\u2026/" + basename
|
||||
|
||||
|
||||
def _abbreviate_url(url: str, max_len: int = 40) -> str:
|
||||
"""Abbreviate a URL keeping domain and filename."""
|
||||
if len(url) <= max_len:
|
||||
return url
|
||||
|
||||
parsed = urlparse(url)
|
||||
domain = parsed.netloc # e.g. "example.com"
|
||||
path_part = parsed.path # e.g. "/api/v2/resource.json"
|
||||
|
||||
# Extract filename from path
|
||||
segments = path_part.rstrip("/").split("/")
|
||||
basename = segments[-1] if segments else ""
|
||||
|
||||
if not basename:
|
||||
# No filename, truncate URL
|
||||
return url[: max_len - 1] + "\u2026"
|
||||
|
||||
budget = max_len - len(domain) - len(basename) - 4 # "…/" + "/"
|
||||
if budget < 0:
|
||||
return domain + "/\u2026" + basename[: max_len - len(domain) - 4]
|
||||
|
||||
# Build abbreviated path
|
||||
kept: list[str] = []
|
||||
for seg in reversed(segments[:-1]):
|
||||
if len(seg) + 1 <= budget:
|
||||
kept.append(seg)
|
||||
budget -= len(seg) + 1
|
||||
else:
|
||||
break
|
||||
|
||||
kept.reverse()
|
||||
if kept:
|
||||
return domain + "/\u2026/" + "/".join(kept) + "/" + basename
|
||||
return domain + "/\u2026/" + basename
|
||||
Reference in New Issue
Block a user