1212from textual .app import App , ComposeResult
1313from textual .binding import Binding
1414from textual .containers import Horizontal , Vertical , VerticalScroll
15+ from textual .css .query import NoMatches
1516from textual .screen import ModalScreen
1617from textual .theme import Theme
1718from 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+
15271603class 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 )
0 commit comments