dataframe-textual 1.9.0__py3-none-any.whl → 1.12.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,11 +29,11 @@ from .common import (
29
29
  NULL_DISPLAY,
30
30
  RIDX,
31
31
  SUBSCRIPT_DIGITS,
32
+ SUPPORTED_FORMATS,
32
33
  DtypeConfig,
33
34
  format_row,
34
35
  get_next_item,
35
36
  parse_placeholders,
36
- rindex,
37
37
  sleep_async,
38
38
  tentative_expr,
39
39
  validate_expr,
@@ -412,6 +412,15 @@ class DataFrameTable(DataTable):
412
412
  """
413
413
  return self.df.item(self.cursor_row_idx, self.cursor_col_idx)
414
414
 
415
+ @property
416
+ def has_hidden_rows(self) -> bool:
417
+ """Check if there are any hidden rows.
418
+
419
+ Returns:
420
+ bool: True if there are hidden rows, False otherwise.
421
+ """
422
+ return any(v for v in self.visible_rows if v is False)
423
+
415
424
  @property
416
425
  def ordered_selected_rows(self) -> list[int]:
417
426
  """Get the list of selected row indices in order.
@@ -846,7 +855,12 @@ class DataFrameTable(DataTable):
846
855
  cell_str = str(self.df.item(ridx, cidx))
847
856
  self.do_copy_to_clipboard(cell_str, f"Copied: [$success]{cell_str[:50]}[/]")
848
857
  except IndexError:
849
- self.notify("Error copying cell", title="Clipboard", severity="error")
858
+ self.notify(
859
+ f"Error copying cell ([$error]{ridx}[/], [$accent]{cidx}[/])",
860
+ title="Clipboard",
861
+ severity="error",
862
+ timeout=10,
863
+ )
850
864
 
851
865
  def action_copy_column(self) -> None:
852
866
  """Copy the current column to clipboard (one value per line)."""
@@ -862,7 +876,7 @@ class DataFrameTable(DataTable):
862
876
  f"Copied [$accent]{len(col_values)}[/] values from column [$success]{col_name}[/]",
863
877
  )
864
878
  except (FileNotFoundError, IndexError):
865
- self.notify("Error copying column", title="Clipboard", severity="error")
879
+ self.notify(f"Error copying column [$error]{col_name}[/]", title="Clipboard", severity="error", timeout=10)
866
880
 
867
881
  def action_copy_row(self) -> None:
868
882
  """Copy the current row to clipboard (values separated by tabs)."""
@@ -878,7 +892,7 @@ class DataFrameTable(DataTable):
878
892
  f"Copied row [$accent]{ridx + 1}[/] with [$success]{len(row_values)}[/] values",
879
893
  )
880
894
  except (FileNotFoundError, IndexError):
881
- self.notify("Error copying row", title="Clipboard", severity="error")
895
+ self.notify(f"Error copying row [$error]{ridx}[/]", title="Clipboard", severity="error", timeout=10)
882
896
 
883
897
  def action_show_thousand_separator(self) -> None:
884
898
  """Toggle thousand separator for numeric display."""
@@ -916,6 +930,27 @@ class DataFrameTable(DataTable):
916
930
  self.check_and_load_more()
917
931
 
918
932
  # Setup & Loading
933
+ def reset_df(self, new_df: pl.DataFrame, dirty: bool = True) -> None:
934
+ """Reset the dataframe to a new one and refresh the table.
935
+
936
+ Args:
937
+ new_df: The new Polars DataFrame to set.
938
+ dirty: Whether to mark the table as dirty (unsaved changes). Defaults to True.
939
+ """
940
+ # Set new dataframe and reset table
941
+ self.df = new_df
942
+ self.loaded_rows = 0
943
+ self.sorted_columns = {}
944
+ self.hidden_columns = set()
945
+ self.selected_rows = [False] * len(self.df)
946
+ self.visible_rows = [True] * len(self.df)
947
+ self.fixed_rows = 0
948
+ self.fixed_columns = 0
949
+ self.matches = defaultdict(set)
950
+ # self.histories.clear()
951
+ # self.history = None
952
+ self.dirty = dirty # Mark as dirty since data changed
953
+
919
954
  def setup_table(self, reset: bool = False) -> None:
920
955
  """Setup the table for display.
921
956
 
@@ -927,21 +962,10 @@ class DataFrameTable(DataTable):
927
962
 
928
963
  # Reset to original dataframe
929
964
  if reset:
930
- self.df = self.dataframe
931
- self.loaded_rows = 0
932
- self.sorted_columns = {}
933
- self.hidden_columns = set()
934
- self.selected_rows = [False] * len(self.df)
935
- self.visible_rows = [True] * len(self.df)
936
- self.fixed_rows = 0
937
- self.fixed_columns = 0
938
- self.matches = defaultdict(set)
939
- self.histories.clear()
940
- self.history = None
941
- self.dirty = False
965
+ self.reset_df(self.dataframe, dirty=False)
942
966
 
943
967
  # Lazy load up to INITIAL_BATCH_SIZE visible rows
944
- stop, visible_count = self.INITIAL_BATCH_SIZE, 0
968
+ stop, visible_count, row_idx = self.INITIAL_BATCH_SIZE, 0, 0
945
969
  for row_idx, visible in enumerate(self.visible_rows):
946
970
  if not visible:
947
971
  continue
@@ -1175,7 +1199,7 @@ class DataFrameTable(DataTable):
1175
1199
  self.log(f"Loaded {self.loaded_rows}/{len(self.df)} rows from `{self.filename or self.name}`")
1176
1200
 
1177
1201
  except Exception as e:
1178
- self.notify("Error loading rows", title="Load", severity="error")
1202
+ self.notify("Error loading rows", title="Load", severity="error", timeout=10)
1179
1203
  self.log(f"Error loading rows: {str(e)}")
1180
1204
 
1181
1205
  def check_and_load_more(self) -> None:
@@ -1191,56 +1215,6 @@ class DataFrameTable(DataTable):
1191
1215
  if bottom_visible_row >= self.loaded_rows - 10:
1192
1216
  self.load_rows(self.loaded_rows + self.BATCH_SIZE)
1193
1217
 
1194
- # Highlighting
1195
- def apply_highlight(self, force: bool = False) -> None:
1196
- """Update all rows, highlighting selected ones and restoring others to default.
1197
-
1198
- Args:
1199
- force: If True, clear all highlights and restore default styles.
1200
- """
1201
- # Ensure all selected rows or matches are loaded
1202
- stop = rindex(self.selected_rows, True) + 1
1203
- stop = max(stop, max(self.matches.keys(), default=0) + 1)
1204
-
1205
- self.load_rows(stop)
1206
- self.highlight_table(force)
1207
-
1208
- def highlight_table(self, force: bool = False) -> None:
1209
- """Highlight selected rows/cells in red."""
1210
- if not force and not any(self.selected_rows) and not self.matches:
1211
- return # Nothing to highlight
1212
-
1213
- # Update all rows based on selected state
1214
- for row in self.ordered_rows:
1215
- ridx = int(row.key.value) # 0-based index
1216
- is_selected = self.selected_rows[ridx]
1217
- match_cols = self.matches.get(ridx, set())
1218
-
1219
- if not force and not is_selected and not match_cols:
1220
- continue # No highlight needed for this row
1221
-
1222
- # Update all cells in this row
1223
- for col_idx, col in enumerate(self.ordered_columns):
1224
- if not force and not is_selected and col_idx not in match_cols:
1225
- continue # No highlight needed for this cell
1226
-
1227
- cell_text: Text = self.get_cell(row.key, col.key)
1228
- need_update = False
1229
-
1230
- if is_selected or col_idx in match_cols:
1231
- cell_text.style = HIGHLIGHT_COLOR
1232
- need_update = True
1233
- elif force:
1234
- # Restore original style based on dtype
1235
- dtype = self.df.schema[col.key.value]
1236
- dc = DtypeConfig(dtype)
1237
- cell_text.style = dc.style
1238
- need_update = True
1239
-
1240
- # Update the cell in the table
1241
- if need_update:
1242
- self.update_cell(row.key, col.key, cell_text)
1243
-
1244
1218
  # History & Undo
1245
1219
  def create_history(self, description: str) -> None:
1246
1220
  """Create the initial history state."""
@@ -1311,7 +1285,7 @@ class DataFrameTable(DataTable):
1311
1285
  # Restore state
1312
1286
  self.apply_history(history)
1313
1287
 
1314
- self.notify(f"Reverted: {history.description}", title="Undo")
1288
+ self.notify(f"Reverted: [$success]{history.description}[/]", title="Undo")
1315
1289
 
1316
1290
  def do_redo(self) -> None:
1317
1291
  """Redo the last undone action."""
@@ -1330,7 +1304,7 @@ class DataFrameTable(DataTable):
1330
1304
  # Clear redo state
1331
1305
  self.history = None
1332
1306
 
1333
- self.notify(f"Reapplied: {description}", title="Redo")
1307
+ self.notify(f"Reapplied: [$success]{description}[/]", title="Redo")
1334
1308
 
1335
1309
  def do_reset(self) -> None:
1336
1310
  """Reset the table to the initial state."""
@@ -1396,7 +1370,7 @@ class DataFrameTable(DataTable):
1396
1370
  fixed_rows, fixed_columns = result
1397
1371
 
1398
1372
  # Add to history
1399
- self.add_history(f"Pinned [$accent]{fixed_rows}[/] rows and [$success]{fixed_columns}[/] columns")
1373
+ self.add_history(f"Pinned [$success]{fixed_rows}[/] rows and [$accent]{fixed_columns}[/] columns")
1400
1374
 
1401
1375
  # Apply the pin settings to the table
1402
1376
  if fixed_rows >= 0:
@@ -1404,7 +1378,7 @@ class DataFrameTable(DataTable):
1404
1378
  if fixed_columns >= 0:
1405
1379
  self.fixed_columns = fixed_columns
1406
1380
 
1407
- # self.notify(f"Pinned [$accent]{fixed_rows}[/] rows and [$success]{fixed_columns}[/] columns", title="Pin")
1381
+ # self.notify(f"Pinned [$success]{fixed_rows}[/] rows and [$accent]{fixed_columns}[/] columns", title="Pin")
1408
1382
 
1409
1383
  def do_hide_column(self) -> None:
1410
1384
  """Hide the currently selected column from the table display."""
@@ -1425,7 +1399,7 @@ class DataFrameTable(DataTable):
1425
1399
  if col_idx >= len(self.columns):
1426
1400
  self.move_cursor(column=len(self.columns) - 1)
1427
1401
 
1428
- # self.notify(f"Hid column [$accent]{col_name}[/]. Press [$success]H[/] to show hidden columns", title="Hide")
1402
+ # self.notify(f"Hid column [$success]{col_name}[/]. Press [$accent]H[/] to show hidden columns", title="Hide")
1429
1403
 
1430
1404
  def do_expand_column(self) -> None:
1431
1405
  """Expand the current column to show the widest cell in the loaded data."""
@@ -1461,7 +1435,9 @@ class DataFrameTable(DataTable):
1461
1435
 
1462
1436
  # self.notify(f"Expanded column [$success]{col_name}[/] to width [$accent]{max_width}[/]", title="Expand")
1463
1437
  except Exception as e:
1464
- self.notify("Error expanding column", title="Expand", severity="error")
1438
+ self.notify(
1439
+ f"Error expanding column [$error]{col_name}[/]", title="Expand Column", severity="error", timeout=10
1440
+ )
1465
1441
  self.log(f"Error expanding column `{col_name}`: {str(e)}")
1466
1442
 
1467
1443
  def do_show_hidden_rows_columns(self) -> None:
@@ -1487,7 +1463,7 @@ class DataFrameTable(DataTable):
1487
1463
  self.setup_table()
1488
1464
 
1489
1465
  self.notify(
1490
- f"Showed [$accent]{hidden_row_count}[/] hidden row(s) and/or [$accent]{hidden_col_count}[/] column(s)",
1466
+ f"Showed [$success]{hidden_row_count}[/] hidden row(s) and/or [$accent]{hidden_col_count}[/] column(s)",
1491
1467
  title="Show",
1492
1468
  )
1493
1469
 
@@ -1595,10 +1571,15 @@ class DataFrameTable(DataTable):
1595
1571
  col_key = col_name
1596
1572
  self.update_cell(row_key, col_key, formatted_value, update_width=True)
1597
1573
 
1598
- # self.notify(f"Cell updated to [$success]{cell_value}[/]", title="Edit")
1574
+ # self.notify(f"Cell updated to [$success]{cell_value}[/]", title="Edit Cell")
1599
1575
  except Exception as e:
1600
- self.notify("Error updating cell", title="Edit", severity="error")
1601
- self.log(f"Error updating cell: {str(e)}")
1576
+ self.notify(
1577
+ f"Error updating cell ([$error]{ridx}[/], [$accent]{col_name}[/])",
1578
+ title="Edit Cell",
1579
+ severity="error",
1580
+ timeout=10,
1581
+ )
1582
+ self.log(f"Error updating cell ({ridx}, {col_name}): {str(e)}")
1602
1583
 
1603
1584
  def do_edit_column(self) -> None:
1604
1585
  """Open modal to edit the entire column with an expression."""
@@ -1627,7 +1608,9 @@ class DataFrameTable(DataTable):
1627
1608
  try:
1628
1609
  expr = validate_expr(term, self.df.columns, cidx)
1629
1610
  except Exception as e:
1630
- self.notify(f"Error validating expression [$error]{term}[/]", title="Edit", severity="error")
1611
+ self.notify(
1612
+ f"Error validating expression [$error]{term}[/]", title="Edit Column", severity="error", timeout=10
1613
+ )
1631
1614
  self.log(f"Error validating expression `{term}`: {str(e)}")
1632
1615
  return
1633
1616
 
@@ -1639,14 +1622,14 @@ class DataFrameTable(DataTable):
1639
1622
  expr = pl.lit(value)
1640
1623
  except Exception:
1641
1624
  self.notify(
1642
- f"Error converting [$accent]{term}[/] to [$error]{dtype}[/]. Cast to string.",
1625
+ f"Error converting [$error]{term}[/] to [$accent]{dtype}[/]. Cast to string.",
1643
1626
  title="Edit",
1644
1627
  severity="error",
1645
1628
  )
1646
1629
  expr = pl.lit(str(term))
1647
1630
 
1648
1631
  # Add to history
1649
- self.add_history(f"Edited column [$accent]{col_name}[/] with expression", dirty=True)
1632
+ self.add_history(f"Edited column [$success]{col_name}[/] with expression", dirty=True)
1650
1633
 
1651
1634
  try:
1652
1635
  # Apply the expression to the column
@@ -1656,6 +1639,7 @@ class DataFrameTable(DataTable):
1656
1639
  f"Error applying expression: [$error]{term}[/] to column [$accent]{col_name}[/]",
1657
1640
  title="Edit",
1658
1641
  severity="error",
1642
+ timeout=10,
1659
1643
  )
1660
1644
  self.log(f"Error applying expression `{term}` to column `{col_name}`: {str(e)}")
1661
1645
  return
@@ -1663,7 +1647,7 @@ class DataFrameTable(DataTable):
1663
1647
  # Recreate table for display
1664
1648
  self.setup_table()
1665
1649
 
1666
- # self.notify(f"Column [$accent]{col_name}[/] updated with [$success]{expr}[/]", title="Edit")
1650
+ # self.notify(f"Column [$accent]{col_name}[/] updated with [$success]{expr}[/]", title="Edit Column")
1667
1651
 
1668
1652
  def do_rename_column(self) -> None:
1669
1653
  """Open modal to rename the selected column."""
@@ -1690,7 +1674,7 @@ class DataFrameTable(DataTable):
1690
1674
  return
1691
1675
 
1692
1676
  # Add to history
1693
- self.add_history(f"Renamed column [$accent]{col_name}[/] to [$success]{new_name}[/]", dirty=True)
1677
+ self.add_history(f"Renamed column [$success]{col_name}[/] to [$accent]{new_name}[/]", dirty=True)
1694
1678
 
1695
1679
  # Rename the column in the dataframe
1696
1680
  self.df = self.df.rename({col_name: new_name})
@@ -1738,10 +1722,15 @@ class DataFrameTable(DataTable):
1738
1722
 
1739
1723
  self.update_cell(row_key, col_key, formatted_value)
1740
1724
 
1741
- # self.notify(f"Cell cleared to [$success]{NULL_DISPLAY}[/]", title="Clear")
1725
+ # self.notify(f"Cell cleared to [$success]{NULL_DISPLAY}[/]", title="Clear Cell")
1742
1726
  except Exception as e:
1743
- self.notify("Error clearing cell", title="Clear", severity="error")
1744
- self.log(f"Error clearing cell: {str(e)}")
1727
+ self.notify(
1728
+ f"Error clearing cell ([$error]{ridx}[/], [$accent]{col_name}[/])",
1729
+ title="Clear Cell",
1730
+ severity="error",
1731
+ timeout=10,
1732
+ )
1733
+ self.log(f"Error clearing cell ({ridx}, {col_name}): {str(e)}")
1745
1734
  raise e
1746
1735
 
1747
1736
  def do_add_column(self, col_name: str = None, col_value: pl.Expr = None) -> None:
@@ -1760,7 +1749,7 @@ class DataFrameTable(DataTable):
1760
1749
  new_name = col_name
1761
1750
 
1762
1751
  # Add to history
1763
- self.add_history(f"Added column [$success]{new_name}[/] after column {cidx + 1}", dirty=True)
1752
+ self.add_history(f"Added column [$success]{new_name}[/] after column [$accent]{cidx + 1}[/]", dirty=True)
1764
1753
 
1765
1754
  try:
1766
1755
  # Create an empty column (all None values)
@@ -1786,8 +1775,8 @@ class DataFrameTable(DataTable):
1786
1775
 
1787
1776
  # self.notify(f"Added column [$success]{new_name}[/]", title="Add Column")
1788
1777
  except Exception as e:
1789
- self.notify("Error adding column", title="Add Column", severity="error")
1790
- self.log(f"Error adding column: {str(e)}")
1778
+ self.notify(f"Error adding column [$error]{new_name}[/]", title="Add Column", severity="error", timeout=10)
1779
+ self.log(f"Error adding column `{new_name}`: {str(e)}")
1791
1780
  raise e
1792
1781
 
1793
1782
  def do_add_column_expr(self) -> None:
@@ -1806,7 +1795,7 @@ class DataFrameTable(DataTable):
1806
1795
  cidx, new_col_name, expr = result
1807
1796
 
1808
1797
  # Add to history
1809
- self.add_history(f"Added column [$success]{new_col_name}[/] with expression {expr}.", dirty=True)
1798
+ self.add_history(f"Added column [$success]{new_col_name}[/] with expression [$accent]{expr}[/].", dirty=True)
1810
1799
 
1811
1800
  try:
1812
1801
  # Create the column
@@ -1829,7 +1818,9 @@ class DataFrameTable(DataTable):
1829
1818
 
1830
1819
  # self.notify(f"Added column [$success]{col_name}[/]", title="Add Column")
1831
1820
  except Exception as e:
1832
- self.notify("Error adding column", title="Add Column", severity="error")
1821
+ self.notify(
1822
+ f"Error adding column [$error]{new_col_name}[/]", title="Add Column", severity="error", timeout=10
1823
+ )
1833
1824
  self.log(f"Error adding column `{new_col_name}`: {str(e)}")
1834
1825
 
1835
1826
  def do_add_link_column(self) -> None:
@@ -1858,7 +1849,7 @@ class DataFrameTable(DataTable):
1858
1849
  cidx, new_col_name, link_template = result
1859
1850
 
1860
1851
  self.add_history(
1861
- f"Added link column [$accent]{new_col_name}[/] with template [$success]{link_template}[/].", dirty=True
1852
+ f"Added link column [$success]{new_col_name}[/] with template [$accent]{link_template}[/].", dirty=True
1862
1853
  )
1863
1854
 
1864
1855
  try:
@@ -1894,7 +1885,9 @@ class DataFrameTable(DataTable):
1894
1885
  self.notify(f"Added link column [$success]{new_col_name}[/]. Use Ctrl/Cmd click to open.", title="Add Link")
1895
1886
 
1896
1887
  except Exception as e:
1897
- self.notify(f"Error adding link column [$error]{new_col_name}[/]", title="Add Link", severity="error")
1888
+ self.notify(
1889
+ f"Error adding link column [$error]{new_col_name}[/]", title="Add Link", severity="error", timeout=10
1890
+ )
1898
1891
  self.log(f"Error adding link column: {str(e)}")
1899
1892
 
1900
1893
  def do_delete_column(self, more: str = None) -> None:
@@ -1999,7 +1992,7 @@ class DataFrameTable(DataTable):
1999
1992
  # Move cursor to the new duplicated column
2000
1993
  self.move_cursor(column=col_idx + 1)
2001
1994
 
2002
- # self.notify(f"Duplicated column [$accent]{col_name}[/] as [$success]{new_col_name}[/]", title="Duplicate")
1995
+ # self.notify(f"Duplicated column [$success]{col_name}[/] as [$accent]{new_col_name}[/]", title="Duplicate")
2003
1996
 
2004
1997
  def do_delete_row(self, more: str = None) -> None:
2005
1998
  """Delete rows from the table and dataframe.
@@ -2046,7 +2039,7 @@ class DataFrameTable(DataTable):
2046
2039
  try:
2047
2040
  df = self.df.with_row_index(RIDX).filter(predicates)
2048
2041
  except Exception as e:
2049
- self.notify(f"Error deleting row(s): {e}", title="Delete", severity="error")
2042
+ self.notify(f"Error deleting row(s): {e}", title="Delete", severity="error", timeout=10)
2050
2043
  self.histories.pop() # Remove last history entry
2051
2044
  return
2052
2045
 
@@ -2065,7 +2058,7 @@ class DataFrameTable(DataTable):
2065
2058
 
2066
2059
  deleted_count = old_count - len(self.df)
2067
2060
  if deleted_count > 0:
2068
- self.notify(f"Deleted [$accent]{deleted_count}[/] row(s)", title="Delete")
2061
+ self.notify(f"Deleted [$success]{deleted_count}[/] row(s)", title="Delete")
2069
2062
 
2070
2063
  def do_duplicate_row(self) -> None:
2071
2064
  """Duplicate the currently selected row, inserting it right after the current row."""
@@ -2137,7 +2130,8 @@ class DataFrameTable(DataTable):
2137
2130
 
2138
2131
  # Add to history
2139
2132
  self.add_history(
2140
- f"Moved column [$success]{col_name}[/] {direction} (swapped with [$success]{swap_name}[/])", dirty=True
2133
+ f"Moved column [$success]{col_name}[/] [$accent]{direction}[/] (swapped with [$success]{swap_name}[/])",
2134
+ dirty=True,
2141
2135
  )
2142
2136
 
2143
2137
  # Swap columns in the table's internal column locations
@@ -2192,7 +2186,7 @@ class DataFrameTable(DataTable):
2192
2186
 
2193
2187
  # Add to history
2194
2188
  self.add_history(
2195
- f"Moved row [$success]{row_key.value}[/] {direction} (swapped with row [$success]{swap_key.value}[/])",
2189
+ f"Moved row [$success]{row_key.value}[/] [$accent]{direction}[/] (swapped with row [$success]{swap_key.value}[/])",
2196
2190
  dirty=True,
2197
2191
  )
2198
2192
 
@@ -2244,12 +2238,12 @@ class DataFrameTable(DataTable):
2244
2238
  try:
2245
2239
  target_dtype = eval(dtype)
2246
2240
  except Exception:
2247
- self.notify(f"Invalid target data type: [$error]{dtype}[/]", title="Cast", severity="error")
2241
+ self.notify(f"Invalid target data type: [$error]{dtype}[/]", title="Cast", severity="error", timeout=10)
2248
2242
  return
2249
2243
 
2250
2244
  if current_dtype == target_dtype:
2251
2245
  self.notify(
2252
- f"Column [$accent]{col_name}[/] is already of type [$success]{target_dtype}[/]",
2246
+ f"Column [$warning]{col_name}[/] is already of type [$accent]{target_dtype}[/]",
2253
2247
  title="Cast",
2254
2248
  severity="warning",
2255
2249
  )
@@ -2257,7 +2251,7 @@ class DataFrameTable(DataTable):
2257
2251
 
2258
2252
  # Add to history
2259
2253
  self.add_history(
2260
- f"Cast column [$accent]{col_name}[/] from [$success]{current_dtype}[/] to [$success]{target_dtype}[/]",
2254
+ f"Cast column [$success]{col_name}[/] from [$accent]{current_dtype}[/] to [$success]{target_dtype}[/]",
2261
2255
  dirty=True,
2262
2256
  )
2263
2257
 
@@ -2268,12 +2262,13 @@ class DataFrameTable(DataTable):
2268
2262
  # Recreate table for display
2269
2263
  self.setup_table()
2270
2264
 
2271
- self.notify(f"Cast column [$accent]{col_name}[/] to [$success]{target_dtype}[/]", title="Cast")
2265
+ self.notify(f"Cast column [$success]{col_name}[/] to [$accent]{target_dtype}[/]", title="Cast")
2272
2266
  except Exception as e:
2273
2267
  self.notify(
2274
- f"Error casting column [$accent]{col_name}[/] to [$error]{target_dtype}[/]",
2268
+ f"Error casting column [$error]{col_name}[/] to [$accent]{target_dtype}[/]",
2275
2269
  title="Cast",
2276
2270
  severity="error",
2271
+ timeout=10,
2277
2272
  )
2278
2273
  self.log(f"Error casting column `{col_name}`: {str(e)}")
2279
2274
 
@@ -2316,7 +2311,9 @@ class DataFrameTable(DataTable):
2316
2311
  try:
2317
2312
  expr = validate_expr(term, self.df.columns, cidx)
2318
2313
  except Exception as e:
2319
- self.notify(f"Error validating expression [$error]{term}[/]", title="Search", severity="error")
2314
+ self.notify(
2315
+ f"Error validating expression [$error]{term}[/]", title="Search", severity="error", timeout=10
2316
+ )
2320
2317
  self.log(f"Error validating expression `{term}`: {str(e)}")
2321
2318
  return
2322
2319
 
@@ -2340,42 +2337,42 @@ class DataFrameTable(DataTable):
2340
2337
  term = f"(?i){term}"
2341
2338
  expr = pl.col(col_name).cast(pl.String).str.contains(term)
2342
2339
  self.notify(
2343
- f"Error converting [$accent]{term}[/] to [$error]{dtype}[/]. Cast to string.",
2340
+ f"Error converting [$error]{term}[/] to [$accent]{dtype}[/]. Cast to string.",
2344
2341
  title="Search",
2345
2342
  severity="warning",
2346
2343
  )
2347
2344
 
2348
2345
  # Lazyframe for filtering
2349
2346
  lf = self.df.lazy().with_row_index(RIDX)
2350
- if False in self.visible_rows:
2347
+ if self.has_hidden_rows:
2351
2348
  lf = lf.filter(self.visible_rows)
2352
2349
 
2353
2350
  # Apply filter to get matched row indices
2354
2351
  try:
2355
2352
  matches = set(lf.filter(expr).select(RIDX).collect().to_series().to_list())
2356
2353
  except Exception as e:
2357
- self.notify(f"Error applying search filter [$error]{term}[/]", title="Search", severity="error")
2354
+ self.notify(f"Error applying search filter [$error]{term}[/]", title="Search", severity="error", timeout=10)
2358
2355
  self.log(f"Error applying search filter `{term}`: {str(e)}")
2359
2356
  return
2360
2357
 
2361
2358
  match_count = len(matches)
2362
2359
  if match_count == 0:
2363
2360
  self.notify(
2364
- f"No matches found for [$accent]{term}[/]. Try [$warning](?i)abc[/] for case-insensitive search.",
2361
+ f"No matches found for [$warning]{term}[/]. Try [$accent](?i)abc[/] for case-insensitive search.",
2365
2362
  title="Search",
2366
2363
  severity="warning",
2367
2364
  )
2368
2365
  return
2369
2366
 
2370
2367
  # Add to history
2371
- self.add_history(f"Searched [$accent]{term}[/] in column [$success]{col_name}[/]")
2368
+ self.add_history(f"Searched [$success]{term}[/] in column [$accent]{col_name}[/]")
2372
2369
 
2373
2370
  # Update selected rows to include new matches
2374
2371
  for m in matches:
2375
2372
  self.selected_rows[m] = True
2376
2373
 
2377
2374
  # Show notification immediately, then start highlighting
2378
- self.notify(f"Found [$accent]{match_count}[/] matches for [$success]{term}[/]", title="Search")
2375
+ self.notify(f"Found [$success]{match_count}[/] matches for [$accent]{term}[/]", title="Search")
2379
2376
 
2380
2377
  # Recreate table for display
2381
2378
  self.setup_table()
@@ -2402,7 +2399,7 @@ class DataFrameTable(DataTable):
2402
2399
 
2403
2400
  # Lazyframe for filtering
2404
2401
  lf = self.df.lazy().with_row_index(RIDX)
2405
- if False in self.visible_rows:
2402
+ if self.has_hidden_rows:
2406
2403
  lf = lf.filter(self.visible_rows)
2407
2404
 
2408
2405
  # Determine which columns to search: single column or all columns
@@ -2420,7 +2417,9 @@ class DataFrameTable(DataTable):
2420
2417
  try:
2421
2418
  expr = validate_expr(term, self.df.columns, col_idx)
2422
2419
  except Exception as e:
2423
- self.notify(f"Error validating expression [$error]{term}[/]", title="Find", severity="error")
2420
+ self.notify(
2421
+ f"Error validating expression [$error]{term}[/]", title="Find", severity="error", timeout=10
2422
+ )
2424
2423
  self.log(f"Error validating expression `{term}`: {str(e)}")
2425
2424
  return matches
2426
2425
  else:
@@ -2434,7 +2433,7 @@ class DataFrameTable(DataTable):
2434
2433
  try:
2435
2434
  matched_ridxs = lf.filter(expr).select(RIDX).collect().to_series().to_list()
2436
2435
  except Exception as e:
2437
- self.notify(f"Error applying filter: {expr}", title="Find", severity="error")
2436
+ self.notify(f"Error applying filter: [$error]{expr}[/]", title="Find", severity="error", timeout=10)
2438
2437
  self.log(f"Error applying filter: {str(e)}")
2439
2438
  return matches
2440
2439
 
@@ -2485,27 +2484,27 @@ class DataFrameTable(DataTable):
2485
2484
  try:
2486
2485
  matches = self.find_matches(term, cidx, match_nocase, match_whole)
2487
2486
  except Exception as e:
2488
- self.notify(f"Error finding matches for [$error]{term}[/]", title="Find", severity="error")
2487
+ self.notify(f"Error finding matches for [$error]{term}[/]", title="Find", severity="error", timeout=10)
2489
2488
  self.log(f"Error finding matches for `{term}`: {str(e)}")
2490
2489
  return
2491
2490
 
2492
2491
  if not matches:
2493
2492
  self.notify(
2494
- f"No matches found for [$accent]{term}[/] in current column. Try [$warning](?i)abc[/] for case-insensitive search.",
2493
+ f"No matches found for [$warning]{term}[/] in current column. Try [$accent](?i)abc[/] for case-insensitive search.",
2495
2494
  title="Find",
2496
2495
  severity="warning",
2497
2496
  )
2498
2497
  return
2499
2498
 
2500
2499
  # Add to history
2501
- self.add_history(f"Found [$accent]{term}[/] in column [$success]{col_name}[/]")
2500
+ self.add_history(f"Found [$success]{term}[/] in column [$accent]{col_name}[/]")
2502
2501
 
2503
2502
  # Add to matches and count total
2504
2503
  match_count = sum(len(col_idxs) for col_idxs in matches.values())
2505
2504
  for ridx, col_idxs in matches.items():
2506
2505
  self.matches[ridx].update(col_idxs)
2507
2506
 
2508
- self.notify(f"Found [$accent]{match_count}[/] matches for [$success]{term}[/]", title="Find")
2507
+ self.notify(f"Found [$success]{match_count}[/] matches for [$accent]{term}[/]", title="Find")
2509
2508
 
2510
2509
  # Recreate table for display
2511
2510
  self.setup_table()
@@ -2519,13 +2518,13 @@ class DataFrameTable(DataTable):
2519
2518
  try:
2520
2519
  matches = self.find_matches(term, cidx=None, match_nocase=match_nocase, match_whole=match_whole)
2521
2520
  except Exception as e:
2522
- self.notify(f"Error finding matches for [$error]{term}[/]", title="Find", severity="error")
2521
+ self.notify(f"Error finding matches for [$error]{term}[/]", title="Find", severity="error", timeout=10)
2523
2522
  self.log(f"Error finding matches for `{term}`: {str(e)}")
2524
2523
  return
2525
2524
 
2526
2525
  if not matches:
2527
2526
  self.notify(
2528
- f"No matches found for [$accent]{term}[/] in any column. Try [$warning](?i)abc[/] for case-insensitive search.",
2527
+ f"No matches found for [$warning]{term}[/] in any column. Try [$accent](?i)abc[/] for case-insensitive search.",
2529
2528
  title="Global Find",
2530
2529
  severity="warning",
2531
2530
  )
@@ -2540,7 +2539,7 @@ class DataFrameTable(DataTable):
2540
2539
  self.matches[ridx].update(col_idxs)
2541
2540
 
2542
2541
  self.notify(
2543
- f"Found [$accent]{match_count}[/] matches for [$success]{term}[/] across all columns", title="Global Find"
2542
+ f"Found [$success]{match_count}[/] matches for [$accent]{term}[/] across all columns", title="Global Find"
2544
2543
  )
2545
2544
 
2546
2545
  # Recreate table for display
@@ -2690,7 +2689,7 @@ class DataFrameTable(DataTable):
2690
2689
 
2691
2690
  # Add to history
2692
2691
  self.add_history(
2693
- f"Replaced [$accent]{term_find}[/] with [$success]{term_replace}[/] in column [$accent]{col_name}[/]"
2692
+ f"Replaced [$success]{term_find}[/] with [$accent]{term_replace}[/] in column [$success]{col_name}[/]"
2694
2693
  )
2695
2694
 
2696
2695
  # Update matches
@@ -2728,9 +2727,10 @@ class DataFrameTable(DataTable):
2728
2727
 
2729
2728
  except Exception as e:
2730
2729
  self.notify(
2731
- f"Error replacing [$accent]{term_find}[/] with [$error]{term_replace}[/]",
2730
+ f"Error replacing [$error]{term_find}[/] with [$accent]{term_replace}[/]",
2732
2731
  title="Replace",
2733
2732
  severity="error",
2733
+ timeout=10,
2734
2734
  )
2735
2735
  self.log(f"Error replacing `{term_find}` with `{term_replace}`: {str(e)}")
2736
2736
 
@@ -2806,7 +2806,7 @@ class DataFrameTable(DataTable):
2806
2806
 
2807
2807
  col_name = "all columns" if state.cidx is None else self.df.columns[state.cidx]
2808
2808
  self.notify(
2809
- f"Replaced [$accent]{state.replaced_occurrence}[/] of [$accent]{state.total_occurrence}[/] in [$success]{col_name}[/]",
2809
+ f"Replaced [$success]{state.replaced_occurrence}[/] of [$accent]{state.total_occurrence}[/] in [$s]{col_name}[/]",
2810
2810
  title="Replace",
2811
2811
  )
2812
2812
 
@@ -2817,9 +2817,10 @@ class DataFrameTable(DataTable):
2817
2817
  self.show_next_replace_confirmation()
2818
2818
  except Exception as e:
2819
2819
  self.notify(
2820
- f"Error replacing [$accent]{term_find}[/] with [$error]{term_replace}[/]",
2820
+ f"Error replacing [$error]{term_find}[/] with [$accent]{term_replace}[/]",
2821
2821
  title="Replace",
2822
2822
  severity="error",
2823
+ timeout=10,
2823
2824
  )
2824
2825
  self.log(f"Error in interactive replace: {str(e)}")
2825
2826
 
@@ -2829,7 +2830,7 @@ class DataFrameTable(DataTable):
2829
2830
  if state.done:
2830
2831
  # All done - show final notification
2831
2832
  col_name = "all columns" if state.cidx is None else self.df.columns[state.cidx]
2832
- msg = f"Replaced [$accent]{state.replaced_occurrence}[/] of [$accent]{state.total_occurrence}[/] in [$success]{col_name}[/]"
2833
+ msg = f"Replaced [$success]{state.replaced_occurrence}[/] of [$accent]{state.total_occurrence}[/] in [$success]{col_name}[/]"
2833
2834
  if state.skipped_occurrence > 0:
2834
2835
  msg += f", [$warning]{state.skipped_occurrence}[/] skipped"
2835
2836
  self.notify(msg, title="Replace")
@@ -2928,7 +2929,7 @@ class DataFrameTable(DataTable):
2928
2929
  # Add to history
2929
2930
  self.add_history("Toggled row selection")
2930
2931
 
2931
- if False in self.visible_rows:
2932
+ if self.has_hidden_rows:
2932
2933
  # Some rows are hidden - invert only selected visible rows and clear selections for hidden rows
2933
2934
  for i in range(len(self.selected_rows)):
2934
2935
  if self.visible_rows[i]:
@@ -2941,7 +2942,7 @@ class DataFrameTable(DataTable):
2941
2942
 
2942
2943
  # Check if we're highlighting or un-highlighting
2943
2944
  if new_selected_count := self.selected_rows.count(True):
2944
- self.notify(f"Toggled selection for [$accent]{new_selected_count}[/] rows", title="Toggle")
2945
+ self.notify(f"Toggled selection for [$success]{new_selected_count}[/] rows", title="Toggle")
2945
2946
 
2946
2947
  # Recreate table for display
2947
2948
  self.setup_table()
@@ -2991,40 +2992,42 @@ class DataFrameTable(DataTable):
2991
2992
  # Recreate table for display
2992
2993
  self.setup_table()
2993
2994
 
2994
- self.notify(f"Cleared selections for [$accent]{row_count}[/] rows", title="Clear")
2995
+ self.notify(f"Cleared selections for [$success]{row_count}[/] rows", title="Clear")
2995
2996
 
2996
2997
  # Filter & View
2997
2998
  def do_filter_rows(self) -> None:
2998
- """Keep only the rows with selections and matches, and remove others."""
2999
- if not any(self.selected_rows) and not self.matches:
3000
- self.notify("No rows to filter", title="Filter", severity="warning")
3001
- return
2999
+ """Keep only the rows with selections and cell matches, and remove others."""
3000
+ if any(self.selected_rows) or self.matches:
3001
+ message = "Filter to rows with selection and cell matches (other rows removed)"
3002
+ filter_expr = [
3003
+ True if (selected or ridx in self.matches) else False
3004
+ for ridx, selected in enumerate(self.selected_rows)
3005
+ ]
3006
+ else: # Search cursor value in current column
3007
+ message = "Filter to rows matching cursor value (other rows removed)"
3008
+ ridx = self.cursor_row_idx
3009
+ cidx = self.cursor_col_idx
3010
+ value = self.df.item(ridx, cidx)
3002
3011
 
3003
- filter_expr = [
3004
- True if (selected or ridx in self.matches) else False for ridx, selected in enumerate(self.selected_rows)
3005
- ]
3012
+ col_name = self.df.columns[cidx]
3013
+ if value is None:
3014
+ filter_expr = pl.col(col_name).is_null()
3015
+ else:
3016
+ filter_expr = pl.col(col_name) == value
3006
3017
 
3007
3018
  # Add to history
3008
- self.add_history("Filtered to selections and matches", dirty=True)
3019
+ self.add_history(message, dirty=True)
3009
3020
 
3010
3021
  # Apply filter to dataframe with row indices
3011
3022
  df_filtered = self.df.with_row_index(RIDX).filter(filter_expr)
3012
3023
 
3013
- # Update selections and matches
3014
- self.selected_rows = [self.selected_rows[ridx] for ridx in df_filtered[RIDX]]
3015
- self.matches = {
3016
- idx: self.matches[ridx].copy() for idx, ridx in enumerate(df_filtered[RIDX]) if ridx in self.matches
3017
- }
3018
-
3019
3024
  # Update dataframe
3020
- self.df = df_filtered.drop(RIDX)
3025
+ self.reset_df(df_filtered.drop(RIDX))
3021
3026
 
3022
3027
  # Recreate table for display
3023
3028
  self.setup_table()
3024
3029
 
3025
- self.notify(
3026
- f"Removed rows without selections or matches. Now showing [$accent]{len(self.df)}[/] rows", title="Filter"
3027
- )
3030
+ self.notify(f"{message}. Now showing [$success]{len(self.df)}[/] rows", title="Filter")
3028
3031
 
3029
3032
  def do_view_rows(self) -> None:
3030
3033
  """View rows.
@@ -3034,6 +3037,7 @@ class DataFrameTable(DataTable):
3034
3037
  """
3035
3038
 
3036
3039
  cidx = self.cursor_col_idx
3040
+ col_name = self.df.columns[cidx]
3037
3041
 
3038
3042
  # If there are rows with selections or matches, use those
3039
3043
  if any(self.selected_rows) or self.matches:
@@ -3043,7 +3047,8 @@ class DataFrameTable(DataTable):
3043
3047
  # Otherwise, use the current cell value
3044
3048
  else:
3045
3049
  ridx = self.cursor_row_idx
3046
- term = str(self.df.item(ridx, cidx))
3050
+ value = self.df.item(ridx, cidx)
3051
+ term = pl.col(col_name).is_null() if value is None else pl.col(col_name) == value
3047
3052
 
3048
3053
  self.view_rows((term, cidx, False, True))
3049
3054
 
@@ -3051,10 +3056,11 @@ class DataFrameTable(DataTable):
3051
3056
  """Open the filter screen to enter an expression."""
3052
3057
  ridx = self.cursor_row_idx
3053
3058
  cidx = self.cursor_col_idx
3054
- cursor_value = str(self.df.item(ridx, cidx))
3059
+ cursor_value = self.df.item(ridx, cidx)
3060
+ term = NULL if cursor_value is None else str(cursor_value)
3055
3061
 
3056
3062
  self.app.push_screen(
3057
- FilterScreen(self.df, cidx, cursor_value),
3063
+ FilterScreen(self.df, cidx, term),
3058
3064
  callback=self.view_rows,
3059
3065
  )
3060
3066
 
@@ -3066,17 +3072,22 @@ class DataFrameTable(DataTable):
3066
3072
 
3067
3073
  col_name = self.df.columns[cidx]
3068
3074
 
3069
- if term == NULL:
3070
- expr = pl.col(col_name).is_null()
3075
+ # Support for polars expression
3076
+ if isinstance(term, pl.Expr):
3077
+ expr = term
3078
+ # Support for list of booleans (selected rows)
3071
3079
  elif isinstance(term, (list, pl.Series)):
3072
- # Support for list of booleans (selected rows)
3073
3080
  expr = term
3081
+ elif term == NULL:
3082
+ expr = pl.col(col_name).is_null()
3074
3083
  elif tentative_expr(term):
3075
- # Support for polars expressions
3084
+ # Support for polars expression in string form
3076
3085
  try:
3077
3086
  expr = validate_expr(term, self.df.columns, cidx)
3078
3087
  except Exception as e:
3079
- self.notify(f"Error validating expression [$error]{term}[/]", title="Filter", severity="error")
3088
+ self.notify(
3089
+ f"Error validating expression [$error]{term}[/]", title="Filter", severity="error", timeout=10
3090
+ )
3080
3091
  self.log(f"Error validating expression `{term}`: {str(e)}")
3081
3092
  return
3082
3093
  else:
@@ -3105,20 +3116,17 @@ class DataFrameTable(DataTable):
3105
3116
  lf = self.df.lazy().with_row_index(RIDX)
3106
3117
 
3107
3118
  # Apply existing visibility filter first
3108
- if False in self.visible_rows:
3119
+ if self.has_hidden_rows:
3109
3120
  lf = lf.filter(self.visible_rows)
3110
3121
 
3111
- if isinstance(expr, (list, pl.Series)):
3112
- expr_str = str(list(expr)[:10]) + ("..." if len(expr) > 10 else "")
3113
- else:
3114
- expr_str = str(expr)
3122
+ expr_str = "boolean list or series" if isinstance(expr, (list, pl.Series)) else str(expr)
3115
3123
 
3116
3124
  # Apply the filter expression
3117
3125
  try:
3118
3126
  df_filtered = lf.filter(expr).collect()
3119
3127
  except Exception as e:
3120
3128
  self.histories.pop() # Remove last history entry
3121
- self.notify(f"Error applying filter [$error]{expr_str}[/]", title="Filter", severity="error")
3129
+ self.notify(f"Error applying filter [$error]{expr_str}[/]", title="Filter", severity="error", timeout=10)
3122
3130
  self.log(f"Error applying filter `{expr_str}`: {str(e)}")
3123
3131
  return
3124
3132
 
@@ -3128,7 +3136,7 @@ class DataFrameTable(DataTable):
3128
3136
  return
3129
3137
 
3130
3138
  # Add to history
3131
- self.add_history(f"Filtered by expression [$success]{expr_str}[/]", dirty=True)
3139
+ self.add_history(f"Filtered by expression [$success]{expr_str}[/]")
3132
3140
 
3133
3141
  # Mark unfiltered rows as invisible
3134
3142
  filtered_row_indices = set(df_filtered[RIDX].to_list())
@@ -3140,7 +3148,7 @@ class DataFrameTable(DataTable):
3140
3148
  # Recreate table for display
3141
3149
  self.setup_table()
3142
3150
 
3143
- self.notify(f"Filtered to [$accent]{matched_count}[/] matching rows", title="Filter")
3151
+ self.notify(f"Filtered to [$success]{matched_count}[/] matching rows", title="Filter")
3144
3152
 
3145
3153
  # Copy & Save
3146
3154
  def do_copy_to_clipboard(self, content: str, message: str) -> None:
@@ -3164,7 +3172,7 @@ class DataFrameTable(DataTable):
3164
3172
  )
3165
3173
  self.notify(message, title="Clipboard")
3166
3174
  except FileNotFoundError:
3167
- self.notify("Error copying to clipboard", title="Clipboard", severity="error")
3175
+ self.notify("Error copying to clipboard", title="Clipboard", severity="error", timeout=10)
3168
3176
 
3169
3177
  def do_save_to_file(
3170
3178
  self, title: str = "Save to File", all_tabs: bool | None = None, task_after_save: str | None = None
@@ -3217,24 +3225,39 @@ class DataFrameTable(DataTable):
3217
3225
  """Actually save the dataframe to a file."""
3218
3226
  filepath = Path(filename)
3219
3227
  ext = filepath.suffix.lower()
3228
+ if ext.endswith(".gz"):
3229
+ ext = Path(filename).with_suffix("").suffix.lower()
3230
+
3231
+ fmt = ext.removeprefix(".")
3232
+ if fmt not in SUPPORTED_FORMATS:
3233
+ self.notify(
3234
+ f"Unsupported file format [$success]{fmt}[/]. Use [$accent]CSV[/] as fallback. Supported formats: {', '.join(SUPPORTED_FORMATS)}",
3235
+ title="Save to File",
3236
+ severity="warning",
3237
+ )
3238
+ fmt = "csv"
3220
3239
 
3221
3240
  # Add to history
3222
3241
  self.add_history(f"Saved dataframe to [$success]{filename}[/]")
3223
3242
 
3224
3243
  try:
3225
- if ext in (".xlsx", ".xls"):
3226
- self.save_excel(filename)
3227
- elif ext in (".tsv", ".tab"):
3244
+ if fmt == "csv":
3245
+ self.df.write_csv(filename)
3246
+ elif fmt in ("tsv", "tab"):
3228
3247
  self.df.write_csv(filename, separator="\t")
3229
- elif ext == ".json":
3248
+ elif fmt in ("xlsx", "xls"):
3249
+ self.save_excel(filename)
3250
+ elif fmt == "json":
3230
3251
  self.df.write_json(filename)
3231
- elif ext == ".parquet":
3252
+ elif fmt == "ndjson":
3253
+ self.df.write_ndjson(filename)
3254
+ elif fmt == "parquet":
3232
3255
  self.df.write_parquet(filename)
3233
- else:
3256
+ else: # Fallback to CSV
3234
3257
  self.df.write_csv(filename)
3235
3258
 
3236
- self.dataframe = self.df # Update original dataframe
3237
- self.filename = filename # Update current filename
3259
+ # Update current filename
3260
+ self.filename = filename
3238
3261
 
3239
3262
  # Reset dirty flag after save
3240
3263
  if self._all_tabs:
@@ -3256,7 +3279,7 @@ class DataFrameTable(DataTable):
3256
3279
  self.notify(f"Saved current tab to [$success]{filename}[/]", title="Save to File")
3257
3280
 
3258
3281
  except Exception as e:
3259
- self.notify(f"Error saving [$error]{filename}[/]", title="Save to File", severity="error")
3282
+ self.notify(f"Error saving [$error]{filename}[/]", title="Save to File", severity="error", timeout=10)
3260
3283
  self.log(f"Error saving file `{filename}`: {str(e)}")
3261
3284
 
3262
3285
  def save_excel(self, filename: str) -> None:
@@ -3325,7 +3348,7 @@ class DataFrameTable(DataTable):
3325
3348
  # Execute the SQL query
3326
3349
  try:
3327
3350
  lf = self.df.lazy().with_row_index(RIDX)
3328
- if False in self.visible_rows:
3351
+ if self.has_hidden_rows:
3329
3352
  lf = lf.filter(self.visible_rows)
3330
3353
 
3331
3354
  df_filtered = lf.sql(sql).collect()
@@ -3337,7 +3360,7 @@ class DataFrameTable(DataTable):
3337
3360
  return
3338
3361
 
3339
3362
  # Add to history
3340
- self.add_history(f"SQL Query:\n[$accent]{sql}[/]", dirty=not view)
3363
+ self.add_history(f"SQL Query:\n[$success]{sql}[/]", dirty=not view)
3341
3364
 
3342
3365
  if view:
3343
3366
  # Just view - do not modify the dataframe