Skip to content

Commit 28b1d78

Browse files
committed
Adds a left-docked filename label in title
1 parent 9fd5b72 commit 28b1d78

2 files changed

Lines changed: 90 additions & 4 deletions

File tree

src/blosc2/b2view/app.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from textual.app import App, ComposeResult
1313
from textual.binding import Binding
1414
from textual.containers import Horizontal, Vertical, VerticalScroll
15+
from textual.css.query import NoMatches
1516
from textual.screen import ModalScreen
1617
from textual.theme import Theme
1718
from textual.widgets import (
@@ -1524,6 +1525,81 @@ def on_progress(downloaded: int, content_total: int | None) -> None:
15241525
self.app.call_from_thread(self.dismiss, True)
15251526

15261527

1528+
class B2ViewHeader(Header):
1529+
"""App header that also shows the open bundle's filename, docked left.
1530+
1531+
Adds a left-docked filename label next to the stock icon plus a right-docked
1532+
spacer of the *same* width, so the title ("b2view — Python-Blosc2 X") stays
1533+
centered across the full header. The two widths are recomputed on resize so
1534+
the title's full text is reserved first and the filename only takes what is
1535+
left over (truncating, then hiding, as the terminal narrows) — the title
1536+
keeps its room as long as possible. Set the name with :meth:`set_filename`.
1537+
"""
1538+
1539+
_ICON_WIDTH = 8 # HeaderIcon dock width
1540+
_CLOCK_WIDTH = 10 # HeaderClockSpace dock width
1541+
1542+
_GAP = 2 # cells kept between the filename and the centered title
1543+
1544+
DEFAULT_CSS = """
1545+
B2ViewHeader #header-filename, B2ViewHeader #header-spacer {
1546+
dock: left;
1547+
text-opacity: 85%;
1548+
text-style: italic;
1549+
text-wrap: nowrap;
1550+
text-overflow: ellipsis;
1551+
content-align: left middle;
1552+
}
1553+
B2ViewHeader #header-spacer {
1554+
dock: right;
1555+
}
1556+
"""
1557+
1558+
def __init__(self, *args, **kwargs) -> None:
1559+
super().__init__(*args, **kwargs)
1560+
self._label = ""
1561+
1562+
def compose(self) -> ComposeResult:
1563+
yield from super().compose()
1564+
yield Static(id="header-filename")
1565+
yield Static(id="header-spacer") # mirrors the filename width to keep the title centered
1566+
1567+
def set_filename(self, label: str) -> None:
1568+
self._label = label
1569+
self._relayout()
1570+
1571+
def on_resize(self) -> None:
1572+
self._relayout()
1573+
1574+
def _relayout(self) -> None:
1575+
"""Size the filename + mirror spacer, reserving the title's width first.
1576+
1577+
No CSS padding (the width math stays in exact cells): a leading space on
1578+
the label provides the gap from the icon.
1579+
"""
1580+
try:
1581+
fname = self.query_one("#header-filename", Static)
1582+
spacer = self.query_one("#header-spacer", Static)
1583+
except NoMatches:
1584+
return # not composed yet
1585+
text = f" {self._label}" if self._label else ""
1586+
fname.update(text)
1587+
title = self.app.title or ""
1588+
sub = self.app.sub_title or ""
1589+
title_len = len(title) + (len(sub) + 3 if sub else 0) # "title — sub"
1590+
# Reserve the icon, clock, the full title and a gap; split the rest
1591+
# symmetrically so the title stays centered and fully visible — the
1592+
# filename takes only the leftover, truncating then hiding as it tightens.
1593+
budget = self.size.width - self._ICON_WIDTH - self._CLOCK_WIDTH - title_len - self._GAP
1594+
each = max(0, budget // 2)
1595+
fname_w = min(len(text), each) if text else 0
1596+
show = fname_w >= 2 # below this there is not even room for " x"
1597+
for widget in (fname, spacer):
1598+
widget.display = show
1599+
if show:
1600+
widget.styles.width = fname_w
1601+
1602+
15271603
class B2ViewApp(App):
15281604
"""Browse TreeStore hierarchy and preview objects."""
15291605

@@ -1587,6 +1663,9 @@ def __init__(
15871663
self.urlpath = urlpath
15881664
self.download_url = download_url # when set, fetch urlpath before browsing
15891665
self.info_url = info_url # optional: metadata endpoint giving the size
1666+
# Header label: the path as given on the CLI, or the @public-relative
1667+
# path for a download (set in on_mount once that is known).
1668+
self._header_label = urlpath
15901669
self.start_path = start_path
15911670
self.start_panel = start_panel
15921671
self.preview_rows = preview_rows
@@ -1608,7 +1687,7 @@ def __init__(
16081687
self.row_window: tuple[int, int] | None = None
16091688

16101689
def compose(self) -> ComposeResult:
1611-
yield Header()
1690+
yield B2ViewHeader()
16121691
with Horizontal(id="main"):
16131692
with B2ViewPanel(id="tree-pane") as tree_pane:
16141693
tree_pane.border_title = "tree"
@@ -1648,6 +1727,7 @@ def on_mount(self) -> None:
16481727
name = os.path.basename(self.urlpath)
16491728
if "/@public/" in self.download_url:
16501729
name = self.download_url.split("/@public/", 1)[1]
1730+
self._header_label = name # @public-relative path for the header
16511731
# A browsable URL for the source root, so the user can see where the
16521732
# file comes from: e.g. https://cat2.cloud/demo/?roots=@public
16531733
source_url = None
@@ -1676,6 +1756,7 @@ def _after_download(self, result: bool | str) -> None:
16761756
def _start_browsing(self) -> None:
16771757
"""Open the bundle and populate the tree (the normal startup path)."""
16781758
self.browser = StoreBrowser(self.urlpath)
1759+
self.query_one(B2ViewHeader).set_filename(self._header_label)
16791760
tree = self.query_one("#tree", Tree)
16801761
tree.root.data = "/"
16811762
self.load_children(tree.root)

tests/b2view/test_basics.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,10 +1118,11 @@ def fake_download(url, dst, on_progress):
11181118
monkeypatch.setattr(app_module, "_http_download", fake_download)
11191119
monkeypatch.setattr(app_module, "_fetch_remote_size", lambda info_url: size)
11201120

1121+
download_url = "https://cat2.cloud/demo/api/download/@public/large/fetched.b2z"
11211122
app = B2ViewApp(
11221123
dest,
1123-
download_url="http://example.test/fetched.b2z",
1124-
info_url="http://example.test/info/fetched.b2z",
1124+
download_url=download_url,
1125+
info_url="https://cat2.cloud/demo/api/info/@public/large/fetched.b2z",
11251126
)
11261127
async with app.run_test(size=TERM_SIZE) as pilot:
11271128
await pilot.pause()
@@ -1137,7 +1138,11 @@ def fake_download(url, dst, on_progress):
11371138
if app.browser is not None:
11381139
break
11391140
# Download finished -> screen dismissed and normal browsing resumed.
1140-
assert calls == [("http://example.test/fetched.b2z", dest)]
1141+
assert calls == [(download_url, dest)]
11411142
assert not isinstance(app.screen, DownloadScreen)
11421143
assert app.browser is not None
11431144
assert len(app.query_one("#tree", Tree).root.children) > 0
1145+
# The header shows the @public-relative path, docked left of the title.
1146+
from textual.widgets import Static
1147+
1148+
assert str(app.query_one("#header-filename", Static).render()).strip() == "large/fetched.b2z"

0 commit comments

Comments
 (0)