diff --git a/src/dt_browser/browser.py b/src/dt_browser/browser.py index 4574d7d..b416e4a 100644 --- a/src/dt_browser/browser.py +++ b/src/dt_browser/browser.py @@ -329,6 +329,7 @@ class DtBrowser(Widget): # pylint: disable=too-many-public-methods,too-many-ins Binding("G", "last_row", "Jump to bottom", show=False), Binding("C", "show_colors", "Colors...", key_display="shift+C"), ("ctrl+s", "show_save", "Save dataframe as..."), + ("w", "toggle_auto_width", "Auto Width"), ] color_by: reactive[tuple[str, ...]] = reactive(tuple(), init=False) @@ -571,6 +572,11 @@ def action_toggle_bookmark(self): async def action_toggle_row_detail(self): self.show_row_detail = not self.show_row_detail + def action_toggle_auto_width(self) -> None: + table = self.query_one("#main_table", CustomTable) + table.auto_width = not table.auto_width + self.notify("Auto column width: " + ("ON" if table.auto_width else "OFF"), timeout=2) + async def action_last_row(self): table = self.query_one("#main_table", CustomTable) coord = table.cursor_coordinate diff --git a/src/dt_browser/custom_table.py b/src/dt_browser/custom_table.py index 4d8527b..8ac8651 100644 --- a/src/dt_browser/custom_table.py +++ b/src/dt_browser/custom_table.py @@ -192,6 +192,7 @@ def control(self) -> CustomTable: COMPONENT_CLASSES: ClassVar[set[str]] = {"datatable--header", "datatable--cursor", "datatable--even-row"} cursor_coordinate: Reactive[Coordinate] = Reactive(Coordinate(0, 0), repaint=False) + auto_width: Reactive[bool] = Reactive(False, repaint=False) def __init__( self, @@ -221,6 +222,8 @@ def __init__( self._render_header_and_table: tuple[Strip, pl.DataFrame] | None = None self._dirty = True + self._full_widths: dict[str, int] = {} + self._auto_width_visible_range: tuple[int, int] | None = None self.set_dt(dt, metadata_dt) @@ -268,6 +271,8 @@ def set_dt(self, dt: pl.DataFrame, metadata_dt: pl.DataFrame): self._dt = dt self._metadata_dt = metadata_dt self._set_widths({x: max(len(x), self._measure(self._dt[x])) for x in self._dt.columns}) + self._full_widths = self._widths.copy() + self._auto_width_visible_range = None self._render_header_and_table = None self._formatters = {x: self._build_cast_expr(x, padding=self._widths[x]) for x in self._dt.columns} self._build_header_contents() @@ -282,6 +287,24 @@ def _set_widths(self, widths: dict[str, int]): for k, v in zip(self._dt.columns, accumulate(x + COL_PADDING for x in self._widths.values()), strict=False) } + def _compute_auto_widths(self, scroll_y: int, dt_height: int) -> dict[str, int]: + """Compute column widths based only on currently visible rows.""" + widths = {} + for col in self._dt.columns: + visible_slice = self._dt[col].slice(scroll_y, dt_height) + data_width = self._measure(visible_slice) if len(visible_slice) > 0 else 0 + widths[col] = max(len(col), data_width) + return widths + + def watch_auto_width(self, value: bool) -> None: + if not value: + self._set_widths(self._full_widths) + self._formatters = {x: self._build_cast_expr(x, padding=self._widths[x]) for x in self._dt.columns} + self._build_header_contents() + self._auto_width_visible_range = None + self._render_header_and_table = None + self.refresh(repaint=True) + def render_line(self, y, *_): if y >= len(self._lines): pad = " " * (self.content_region.width) @@ -357,6 +380,7 @@ def on_resize(self, event: events.Resize): def _ensure_cursor(self, allow_refresh: bool = True): self._render_header_and_table = None + self._auto_width_visible_range = None max_idx = self.cursor_coordinate.column while not self._is_col_visible(max_idx) and max_idx > 0: @@ -496,10 +520,25 @@ def render_header_and_table(self): self._dirty = True scroll_x, scroll_y = self.scroll_offset - cols_to_render: list[str] = [] effective_width = self.scrollable_content_region.width if effective_width <= 2: return (Strip([]), pl.DataFrame()) + + dt_height = self.window_region.height - HEADER_HEIGHT + + if self.auto_width: + visible_range = (scroll_y, dt_height) + if visible_range != self._auto_width_visible_range: + self._auto_width_visible_range = visible_range + new_widths = self._compute_auto_widths(scroll_y, dt_height) + if new_widths != self._widths: + self._set_widths(new_widths) + self._formatters = { + x: self._build_cast_expr(x, padding=self._widths[x]) for x in self._dt.columns + } + self._build_header_contents() + + cols_to_render: list[str] = [] truncate_last: int | None = None for x in self._dt.columns: min_offset = self._cum_widths[x] - scroll_x @@ -518,7 +557,6 @@ def render_header_and_table(self): if not cols_to_render: return (Strip([]), pl.DataFrame()) - dt_height = self.window_region.height - HEADER_HEIGHT base_header, header_width = self._build_base_header(cols_to_render) excess = self.scrollable_content_region.width - header_width header = Strip(base_header + (self._header_pad * (excess))) @@ -584,6 +622,7 @@ def build_selector(cols: list[str], needed_padding: int = 0): ) else: cursor_col_idx = self.cursor_coordinate.column - self._dt.columns.index(visible_cols[0]) + cursor_col_idx = max(0, min(cursor_col_idx, len(visible_cols) - 1)) cols_before_selected: list[str] = visible_cols[0:cursor_col_idx] sel_col = visible_cols[cursor_col_idx] diff --git a/tests/__snapshots__/test_auto_width/test_snap_auto_width_off.svg b/tests/__snapshots__/test_auto_width/test_snap_auto_width_off.svg new file mode 100644 index 0000000..eeae9b4 --- /dev/null +++ b/tests/__snapshots__/test_auto_width/test_snap_auto_width_off.svg @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DtBrowserApp + + + + + + + + + +  Row # short_col growing_col                                                      ▔ Row Detail ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎ +    1 s0        x                                                                 Field       dtype  Value        +    2 s1        xx                                                                Row #       UInt32 1            +    3 s2        xxx                                                               short_col   String s0           +    4 s3        xxxx                                                              growing_col String x            +    5 s4        xxxxx                                                             fixed_col   String fixed_0000   +    6 s5        xxxxxx                                                            +    7 s6        xxxxxxx                                                          ▁▁ +    8 s7        xxxxxxxx                                                          +    9 s8        xxxxxxxxx                                                         +   10 s9        xxxxxxxxxx                                                        +   11 s10       xxxxxxxxxxx                                                       +   12 s11       xxxxxxxxxxxx                                                      +   13 s12       xxxxxxxxxxxxx                                                     +   14 s13       xxxxxxxxxxxxxx                                                    +   15 s14       xxxxxxxxxxxxxxx                                                   +   16 s15       xxxxxxxxxxxxxxxx                                                  +   17 s16       xxxxxxxxxxxxxxxxx                                                 +   18 s17       xxxxxxxxxxxxxxxxxx                                                +   19 s18       xxxxxxxxxxxxxxxxxxx                                               +   20 s19       xxxxxxxxxxxxxxxxxxxx                                              +   21 s20       xxxxxxxxxxxxxxxxxxxxx                                             +   22 s21       xxxxxxxxxxxxxxxxxxxxxx                                            +   23 s22       xxxxxxxxxxxxxxxxxxxxxxx                                           +   24 s23       xxxxxxxxxxxxxxxxxxxxxxxx                                          +   25 s24       xxxxxxxxxxxxxxxxxxxxxxxxx                                         +   26 s25       xxxxxxxxxxxxxxxxxxxxxxxxxx                                        +   27 s26       xxxxxxxxxxxxxxxxxxxxxxxxxxx                                       +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎ + f Filter rows  / Search  b Add/Del Bookmark  x Compute Expressions...  c Columns...  r Toggle Row Detail  s^p palette + + + diff --git a/tests/__snapshots__/test_auto_width/test_snap_auto_width_on.svg b/tests/__snapshots__/test_auto_width/test_snap_auto_width_on.svg new file mode 100644 index 0000000..f2dac31 --- /dev/null +++ b/tests/__snapshots__/test_auto_width/test_snap_auto_width_on.svg @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DtBrowserApp + + + + + + + + + +  Row # short_col growing_col                  fixed_col                           ▔ Row Detail ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎ +    1 s0        x                            fixed_0000                           Field       dtype  Value        +    2 s1        xx                           fixed_0001                           Row #       UInt32 1            +    3 s2        xxx                          fixed_0002                           short_col   String s0           +    4 s3        xxxx                         fixed_0003                           growing_col String x            +    5 s4        xxxxx                        fixed_0004                           fixed_col   String fixed_0000   +    6 s5        xxxxxx                       fixed_0005                           +    7 s6        xxxxxxx                      fixed_0006                           +    8 s7        xxxxxxxx                     fixed_0007                          ▅▅ +    9 s8        xxxxxxxxx                    fixed_0008                           +   10 s9        xxxxxxxxxx                   fixed_0009                           +   11 s10       xxxxxxxxxxx                  fixed_0010                           +   12 s11       xxxxxxxxxxxx                 fixed_0011                           +   13 s12       xxxxxxxxxxxxx                fixed_0012                           +   14 s13       xxxxxxxxxxxxxx               fixed_0013                           +   15 s14       xxxxxxxxxxxxxxx              fixed_0014                           +   16 s15       xxxxxxxxxxxxxxxx             fixed_0015                           +   17 s16       xxxxxxxxxxxxxxxxx            fixed_0016                           +   18 s17       xxxxxxxxxxxxxxxxxx           fixed_0017                           +   19 s18       xxxxxxxxxxxxxxxxxxx          fixed_0018                           +   20 s19       xxxxxxxxxxxxxxxxxxxx         fixed_0019                           +   21 s20       xxxxxxxxxxxxxxxxxxxxx        fixed_0020                           +   22 s21       xxxxxxxxxxxxxxxxxxxxxx       fixed_0021                           +   23 s22       xxxxxxxxxxxxxxxxxxxxxxx      fixed_0022                           +   24 s23       xxxxxxxxxxxxxxxxxxxxxxxx     fixed_0023                           +   25 s24       xxxxxxxxxxxxxxxxxxxxxxxxx    fixed_0024                           +   26 s25       xxxxxxxxxxxxxxxxxxxxxxxxxx   fixed_0025    +   27 s26       xxxxxxxxxxxxxxxxxxxxxxxxxxx  fixed_0026   Auto column width: ON +   28 s27       xxxxxxxxxxxxxxxxxxxxxxxxxxxx fixed_0027   ▁▎ + f Filter rows  / Search  b Add/Del Bookmark  x Compute Expressions...  c Columns...  r Toggle Row Detail  s^p palette + + + diff --git a/tests/__snapshots__/test_auto_width/test_snap_auto_width_on_scrolled_bottom.svg b/tests/__snapshots__/test_auto_width/test_snap_auto_width_on_scrolled_bottom.svg new file mode 100644 index 0000000..1cb5b20 --- /dev/null +++ b/tests/__snapshots__/test_auto_width/test_snap_auto_width_on_scrolled_bottom.svg @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DtBrowserApp + + + + + + + + + +  Row # short_col growing_col                              ▔ Row Detail ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎ +   73 s72       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  Field       dtype  Value                                +   74 s73       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  Row #       UInt32 100                                  +   75 s74       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  short_col   String s99                                  +   76 s75       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  growing_col String xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  +   77 s76       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  fixed_col   String fixed_0099                           +   78 s77       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  +   79 s78       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  +   80 s79       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  +   81 s80       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  +   82 s81       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  +   83 s82       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  +   84 s83       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  +   85 s84       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  +   86 s85       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  +   87 s86       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  +   88 s87       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  +   89 s88       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  +   90 s89       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  +   91 s90       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx... ▁▁ +   92 s91       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  +   93 s92       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  +   94 s93       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  +   95 s94       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  +   96 s95       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  +   97 s96       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  +   98 s97       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...  +   99 s98       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx... Auto column width: ON +▁▎ + f Filter rows  / Search  b Add/Del Bookmark  x Compute Expressions...  c Columns...  r Toggle Row Detail  s^p palette + + + diff --git a/tests/__snapshots__/test_auto_width/test_snap_auto_width_toggled_off_after_on.svg b/tests/__snapshots__/test_auto_width/test_snap_auto_width_toggled_off_after_on.svg new file mode 100644 index 0000000..1470e1e --- /dev/null +++ b/tests/__snapshots__/test_auto_width/test_snap_auto_width_toggled_off_after_on.svg @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DtBrowserApp + + + + + + + + + +  Row # short_col growing_col                                                      ▔ Row Detail ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎ +    1 s0        x                                                                 Field       dtype  Value        +    2 s1        xx                                                                Row #       UInt32 1            +    3 s2        xxx                                                               short_col   String s0           +    4 s3        xxxx                                                              growing_col String x            +    5 s4        xxxxx                                                             fixed_col   String fixed_0000   +    6 s5        xxxxxx                                                            +    7 s6        xxxxxxx                                                          ▁▁ +    8 s7        xxxxxxxx                                                          +    9 s8        xxxxxxxxx                                                         +   10 s9        xxxxxxxxxx                                                        +   11 s10       xxxxxxxxxxx                                                       +   12 s11       xxxxxxxxxxxx                                                      +   13 s12       xxxxxxxxxxxxx                                                     +   14 s13       xxxxxxxxxxxxxx                                                    +   15 s14       xxxxxxxxxxxxxxx                                                   +   16 s15       xxxxxxxxxxxxxxxx                                                  +   17 s16       xxxxxxxxxxxxxxxxx                                                 +   18 s17       xxxxxxxxxxxxxxxxxx                                                +   19 s18       xxxxxxxxxxxxxxxxxxx                                               +   20 s19       xxxxxxxxxxxxxxxxxxxx                                              +   21 s20       xxxxxxxxxxxxxxxxxxxxx                                             +   22 s21       xxxxxxxxxxxxxxxxxxxxxx                     +   23 s22       xxxxxxxxxxxxxxxxxxxxxxx                   Auto column width: ON +   24 s23       xxxxxxxxxxxxxxxxxxxxxxxx                   +   25 s24       xxxxxxxxxxxxxxxxxxxxxxxxx                                         +   26 s25       xxxxxxxxxxxxxxxxxxxxxxxxxx                 +   27 s26       xxxxxxxxxxxxxxxxxxxxxxxxxxx               Auto column width: OFF +▁▎ + f Filter rows  / Search  b Add/Del Bookmark  x Compute Expressions...  c Columns...  r Toggle Row Detail  s^p palette + + + diff --git a/tests/test_auto_width.py b/tests/test_auto_width.py new file mode 100644 index 0000000..5252586 --- /dev/null +++ b/tests/test_auto_width.py @@ -0,0 +1,238 @@ +import polars as pl +from textual.pilot import Pilot + +from dt_browser.browser import DtBrowserApp +from dt_browser.custom_table import CustomTable + + +def _make_varying_width_app(num_rows: int = 50) -> DtBrowserApp: + """Create an app where row data varies in width so auto-width has a visible effect. + + Early rows have short values, later rows have long values. + """ + df = pl.DataFrame( + { + "short_col": [f"s{i}" for i in range(num_rows)], + "growing_col": [f"{'x' * (i + 1)}" for i in range(num_rows)], + "fixed_col": [f"fixed_{i:04d}" for i in range(num_rows)], + } + ) + return DtBrowserApp("test", df) + + +def _make_app_simple(num_rows: int = 20) -> DtBrowserApp: + df = pl.DataFrame( + { + "name": [f"item_{i}" for i in range(num_rows)], + "value": list(range(num_rows)), + "score": [round(i * 1.5, 1) for i in range(num_rows)], + "category": [f"cat_{i % 5}" for i in range(num_rows)], + } + ) + return DtBrowserApp("test", df) + + +async def test_auto_width_toggle(): + """Toggling auto_width on and off updates the reactive property.""" + app = _make_varying_width_app() + async with app.run_test(size=(120, 30)) as pilot: + await pilot.pause() + table = app.query_one("#main_table", CustomTable) + + assert table.auto_width is False + + await pilot.press("w") + await pilot.pause() + assert table.auto_width is True + + await pilot.press("w") + await pilot.pause() + assert table.auto_width is False + + +async def test_auto_width_narrows_columns(): + """When auto_width is on, columns are narrower if visible rows have shorter data.""" + app = _make_varying_width_app(num_rows=100) + async with app.run_test(size=(120, 30)) as pilot: + await pilot.pause() + table = app.query_one("#main_table", CustomTable) + + # Full widths computed from all 100 rows (growing_col goes up to 100 chars) + full_widths = table._widths.copy() + + await pilot.press("w") + await pilot.pause() + + # Auto widths should be narrower for growing_col since visible rows are near the top + auto_widths = table._widths.copy() + assert auto_widths["growing_col"] < full_widths["growing_col"], ( + f"Expected auto width ({auto_widths['growing_col']}) < full width ({full_widths['growing_col']}) " + f"for growing_col when viewing top rows" + ) + + +async def test_auto_width_updates_on_scroll(): + """Scrolling to rows with wider data increases auto column widths.""" + app = _make_varying_width_app(num_rows=100) + async with app.run_test(size=(120, 30)) as pilot: + await pilot.pause() + table = app.query_one("#main_table", CustomTable) + + await pilot.press("w") + await pilot.pause() + widths_at_top = table._widths.copy() + + # Scroll to the bottom where growing_col values are much wider + await pilot.press("G") + await pilot.pause() + + widths_at_bottom = table._widths.copy() + assert widths_at_bottom["growing_col"] > widths_at_top["growing_col"], ( + f"Expected wider growing_col at bottom ({widths_at_bottom['growing_col']}) " + f"than at top ({widths_at_top['growing_col']})" + ) + + +async def test_auto_width_restores_full_widths_on_toggle_off(): + """Toggling auto_width off restores the original precomputed widths.""" + app = _make_varying_width_app(num_rows=100) + async with app.run_test(size=(120, 30)) as pilot: + await pilot.pause() + table = app.query_one("#main_table", CustomTable) + + full_widths = table._widths.copy() + + # Toggle on + await pilot.press("w") + await pilot.pause() + assert table._widths != full_widths # auto widths should differ + + # Toggle off + await pilot.press("w") + await pilot.pause() + assert table._widths == full_widths, "Widths should be restored to full precomputed values" + + +async def test_auto_width_respects_column_name_min_width(): + """Auto width never makes a column narrower than its header name.""" + app = _make_varying_width_app(num_rows=100) + async with app.run_test(size=(120, 30)) as pilot: + await pilot.pause() + table = app.query_one("#main_table", CustomTable) + + await pilot.press("w") + await pilot.pause() + + for col in table._dt.columns: + assert table._widths[col] >= len(col), ( + f"Column '{col}' width ({table._widths[col]}) is less than header name length ({len(col)})" + ) + + +async def test_auto_width_skips_recompute_when_range_unchanged(): + """Moving cursor within visible area does not trigger width recomputation.""" + app = _make_varying_width_app(num_rows=100) + async with app.run_test(size=(120, 30)) as pilot: + await pilot.pause() + table = app.query_one("#main_table", CustomTable) + + await pilot.press("w") + await pilot.pause() + widths_before = table._widths.copy() + range_before = table._auto_width_visible_range + + # Move cursor within visible area + await pilot.press("down") + await pilot.pause() + + assert table._auto_width_visible_range == range_before, "Visible range should not change for in-view cursor move" + assert table._widths == widths_before, "Widths should not change when cursor moves within visible area" + + +async def test_auto_width_with_resize(): + """Auto width recalculates when the terminal is resized.""" + app = _make_varying_width_app(num_rows=100) + async with app.run_test(size=(120, 30)) as pilot: + await pilot.pause() + table = app.query_one("#main_table", CustomTable) + + await pilot.press("w") + await pilot.pause() + range_before = table._auto_width_visible_range + + # Resize changes dt_height, so auto widths should recompute + await pilot.resize_terminal(120, 20) + await pilot.pause() + + # Force a render by accessing the property + _ = table.render_header_and_table + + assert table._auto_width_visible_range != range_before, ( + "Visible range should change after resize" + ) + + +async def test_auto_width_all_null_columns(): + """Auto width does not crash when all visible columns have null values.""" + df = pl.DataFrame( + { + "col_a": [None, None, None, None, None], + "col_b": [None, None, None, None, None], + "col_c": [None, None, None, None, None], + }, + schema={"col_a": pl.Utf8, "col_b": pl.Utf8, "col_c": pl.Utf8}, + ) + app = DtBrowserApp("test", df) + async with app.run_test(size=(120, 30)) as pilot: + await pilot.pause() + table = app.query_one("#main_table", CustomTable) + + # Switch to cell cursor mode then enable auto width + await pilot.press("w") + await pilot.pause() + + # Should not crash — columns should be at least header-name width + for col in table._dt.columns: + assert table._widths[col] >= len(col) + + +# --- Snapshot tests --- + + +def test_snap_auto_width_off(snap_compare): + """Snapshot with auto width OFF (default).""" + assert snap_compare(_make_varying_width_app(num_rows=100), terminal_size=(120, 30)) + + +def test_snap_auto_width_on(snap_compare): + """Snapshot with auto width ON — columns should be narrower at top of data.""" + assert snap_compare( + _make_varying_width_app(num_rows=100), + press=["w"], + terminal_size=(120, 30), + ) + + +def test_snap_auto_width_on_scrolled_bottom(snap_compare): + """Snapshot with auto width ON after scrolling to bottom — columns should be wider.""" + + async def run_before(pilot: Pilot) -> None: + await pilot.press("w") + await pilot.pause() + await pilot.press("G") + await pilot.pause() + + assert snap_compare( + _make_varying_width_app(num_rows=100), + run_before=run_before, + terminal_size=(120, 30), + ) + + +def test_snap_auto_width_toggled_off_after_on(snap_compare): + """Snapshot after toggling auto width ON then OFF — should match original layout.""" + assert snap_compare( + _make_varying_width_app(num_rows=100), + press=["w", "w"], + terminal_size=(120, 30), + )