dataframe-textual 1.10.1__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."""
@@ -933,8 +947,8 @@ class DataFrameTable(DataTable):
933
947
  self.fixed_rows = 0
934
948
  self.fixed_columns = 0
935
949
  self.matches = defaultdict(set)
936
- self.histories.clear()
937
- self.history = None
950
+ # self.histories.clear()
951
+ # self.history = None
938
952
  self.dirty = dirty # Mark as dirty since data changed
939
953
 
940
954
  def setup_table(self, reset: bool = False) -> None:
@@ -951,7 +965,7 @@ class DataFrameTable(DataTable):
951
965
  self.reset_df(self.dataframe, dirty=False)
952
966
 
953
967
  # Lazy load up to INITIAL_BATCH_SIZE visible rows
954
- stop, visible_count = self.INITIAL_BATCH_SIZE, 0
968
+ stop, visible_count, row_idx = self.INITIAL_BATCH_SIZE, 0, 0
955
969
  for row_idx, visible in enumerate(self.visible_rows):
956
970
  if not visible:
957
971
  continue
@@ -1185,7 +1199,7 @@ class DataFrameTable(DataTable):
1185
1199
  self.log(f"Loaded {self.loaded_rows}/{len(self.df)} rows from `{self.filename or self.name}`")
1186
1200
 
1187
1201
  except Exception as e:
1188
- self.notify("Error loading rows", title="Load", severity="error")
1202
+ self.notify("Error loading rows", title="Load", severity="error", timeout=10)
1189
1203
  self.log(f"Error loading rows: {str(e)}")
1190
1204
 
1191
1205
  def check_and_load_more(self) -> None:
@@ -1201,56 +1215,6 @@ class DataFrameTable(DataTable):
1201
1215
  if bottom_visible_row >= self.loaded_rows - 10:
1202
1216
  self.load_rows(self.loaded_rows + self.BATCH_SIZE)
1203
1217
 
1204
- # Highlighting
1205
- def apply_highlight(self, force: bool = False) -> None:
1206
- """Update all rows, highlighting selected ones and restoring others to default.
1207
-
1208
- Args:
1209
- force: If True, clear all highlights and restore default styles.
1210
- """
1211
- # Ensure all selected rows or matches are loaded
1212
- stop = rindex(self.selected_rows, True) + 1
1213
- stop = max(stop, max(self.matches.keys(), default=0) + 1)
1214
-
1215
- self.load_rows(stop)
1216
- self.highlight_table(force)
1217
-
1218
- def highlight_table(self, force: bool = False) -> None:
1219
- """Highlight selected rows/cells in red."""
1220
- if not force and not any(self.selected_rows) and not self.matches:
1221
- return # Nothing to highlight
1222
-
1223
- # Update all rows based on selected state
1224
- for row in self.ordered_rows:
1225
- ridx = int(row.key.value) # 0-based index
1226
- is_selected = self.selected_rows[ridx]
1227
- match_cols = self.matches.get(ridx, set())
1228
-
1229
- if not force and not is_selected and not match_cols:
1230
- continue # No highlight needed for this row
1231
-
1232
- # Update all cells in this row
1233
- for col_idx, col in enumerate(self.ordered_columns):
1234
- if not force and not is_selected and col_idx not in match_cols:
1235
- continue # No highlight needed for this cell
1236
-
1237
- cell_text: Text = self.get_cell(row.key, col.key)
1238
- need_update = False
1239
-
1240
- if is_selected or col_idx in match_cols:
1241
- cell_text.style = HIGHLIGHT_COLOR
1242
- need_update = True
1243
- elif force:
1244
- # Restore original style based on dtype
1245
- dtype = self.df.schema[col.key.value]
1246
- dc = DtypeConfig(dtype)
1247
- cell_text.style = dc.style
1248
- need_update = True
1249
-
1250
- # Update the cell in the table
1251
- if need_update:
1252
- self.update_cell(row.key, col.key, cell_text)
1253
-
1254
1218
  # History & Undo
1255
1219
  def create_history(self, description: str) -> None:
1256
1220
  """Create the initial history state."""
@@ -1321,7 +1285,7 @@ class DataFrameTable(DataTable):
1321
1285
  # Restore state
1322
1286
  self.apply_history(history)
1323
1287
 
1324
- self.notify(f"Reverted: {history.description}", title="Undo")
1288
+ self.notify(f"Reverted: [$success]{history.description}[/]", title="Undo")
1325
1289
 
1326
1290
  def do_redo(self) -> None:
1327
1291
  """Redo the last undone action."""
@@ -1340,7 +1304,7 @@ class DataFrameTable(DataTable):
1340
1304
  # Clear redo state
1341
1305
  self.history = None
1342
1306
 
1343
- self.notify(f"Reapplied: {description}", title="Redo")
1307
+ self.notify(f"Reapplied: [$success]{description}[/]", title="Redo")
1344
1308
 
1345
1309
  def do_reset(self) -> None:
1346
1310
  """Reset the table to the initial state."""
@@ -1406,7 +1370,7 @@ class DataFrameTable(DataTable):
1406
1370
  fixed_rows, fixed_columns = result
1407
1371
 
1408
1372
  # Add to history
1409
- 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")
1410
1374
 
1411
1375
  # Apply the pin settings to the table
1412
1376
  if fixed_rows >= 0:
@@ -1414,7 +1378,7 @@ class DataFrameTable(DataTable):
1414
1378
  if fixed_columns >= 0:
1415
1379
  self.fixed_columns = fixed_columns
1416
1380
 
1417
- # 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")
1418
1382
 
1419
1383
  def do_hide_column(self) -> None:
1420
1384
  """Hide the currently selected column from the table display."""
@@ -1435,7 +1399,7 @@ class DataFrameTable(DataTable):
1435
1399
  if col_idx >= len(self.columns):
1436
1400
  self.move_cursor(column=len(self.columns) - 1)
1437
1401
 
1438
- # 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")
1439
1403
 
1440
1404
  def do_expand_column(self) -> None:
1441
1405
  """Expand the current column to show the widest cell in the loaded data."""
@@ -1471,7 +1435,9 @@ class DataFrameTable(DataTable):
1471
1435
 
1472
1436
  # self.notify(f"Expanded column [$success]{col_name}[/] to width [$accent]{max_width}[/]", title="Expand")
1473
1437
  except Exception as e:
1474
- 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
+ )
1475
1441
  self.log(f"Error expanding column `{col_name}`: {str(e)}")
1476
1442
 
1477
1443
  def do_show_hidden_rows_columns(self) -> None:
@@ -1497,7 +1463,7 @@ class DataFrameTable(DataTable):
1497
1463
  self.setup_table()
1498
1464
 
1499
1465
  self.notify(
1500
- 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)",
1501
1467
  title="Show",
1502
1468
  )
1503
1469
 
@@ -1605,10 +1571,15 @@ class DataFrameTable(DataTable):
1605
1571
  col_key = col_name
1606
1572
  self.update_cell(row_key, col_key, formatted_value, update_width=True)
1607
1573
 
1608
- # self.notify(f"Cell updated to [$success]{cell_value}[/]", title="Edit")
1574
+ # self.notify(f"Cell updated to [$success]{cell_value}[/]", title="Edit Cell")
1609
1575
  except Exception as e:
1610
- self.notify("Error updating cell", title="Edit", severity="error")
1611
- 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)}")
1612
1583
 
1613
1584
  def do_edit_column(self) -> None:
1614
1585
  """Open modal to edit the entire column with an expression."""
@@ -1637,7 +1608,9 @@ class DataFrameTable(DataTable):
1637
1608
  try:
1638
1609
  expr = validate_expr(term, self.df.columns, cidx)
1639
1610
  except Exception as e:
1640
- 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
+ )
1641
1614
  self.log(f"Error validating expression `{term}`: {str(e)}")
1642
1615
  return
1643
1616
 
@@ -1649,14 +1622,14 @@ class DataFrameTable(DataTable):
1649
1622
  expr = pl.lit(value)
1650
1623
  except Exception:
1651
1624
  self.notify(
1652
- f"Error converting [$accent]{term}[/] to [$error]{dtype}[/]. Cast to string.",
1625
+ f"Error converting [$error]{term}[/] to [$accent]{dtype}[/]. Cast to string.",
1653
1626
  title="Edit",
1654
1627
  severity="error",
1655
1628
  )
1656
1629
  expr = pl.lit(str(term))
1657
1630
 
1658
1631
  # Add to history
1659
- 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)
1660
1633
 
1661
1634
  try:
1662
1635
  # Apply the expression to the column
@@ -1666,6 +1639,7 @@ class DataFrameTable(DataTable):
1666
1639
  f"Error applying expression: [$error]{term}[/] to column [$accent]{col_name}[/]",
1667
1640
  title="Edit",
1668
1641
  severity="error",
1642
+ timeout=10,
1669
1643
  )
1670
1644
  self.log(f"Error applying expression `{term}` to column `{col_name}`: {str(e)}")
1671
1645
  return
@@ -1673,7 +1647,7 @@ class DataFrameTable(DataTable):
1673
1647
  # Recreate table for display
1674
1648
  self.setup_table()
1675
1649
 
1676
- # 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")
1677
1651
 
1678
1652
  def do_rename_column(self) -> None:
1679
1653
  """Open modal to rename the selected column."""
@@ -1700,7 +1674,7 @@ class DataFrameTable(DataTable):
1700
1674
  return
1701
1675
 
1702
1676
  # Add to history
1703
- 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)
1704
1678
 
1705
1679
  # Rename the column in the dataframe
1706
1680
  self.df = self.df.rename({col_name: new_name})
@@ -1748,10 +1722,15 @@ class DataFrameTable(DataTable):
1748
1722
 
1749
1723
  self.update_cell(row_key, col_key, formatted_value)
1750
1724
 
1751
- # self.notify(f"Cell cleared to [$success]{NULL_DISPLAY}[/]", title="Clear")
1725
+ # self.notify(f"Cell cleared to [$success]{NULL_DISPLAY}[/]", title="Clear Cell")
1752
1726
  except Exception as e:
1753
- self.notify("Error clearing cell", title="Clear", severity="error")
1754
- 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)}")
1755
1734
  raise e
1756
1735
 
1757
1736
  def do_add_column(self, col_name: str = None, col_value: pl.Expr = None) -> None:
@@ -1770,7 +1749,7 @@ class DataFrameTable(DataTable):
1770
1749
  new_name = col_name
1771
1750
 
1772
1751
  # Add to history
1773
- 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)
1774
1753
 
1775
1754
  try:
1776
1755
  # Create an empty column (all None values)
@@ -1796,8 +1775,8 @@ class DataFrameTable(DataTable):
1796
1775
 
1797
1776
  # self.notify(f"Added column [$success]{new_name}[/]", title="Add Column")
1798
1777
  except Exception as e:
1799
- self.notify("Error adding column", title="Add Column", severity="error")
1800
- 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)}")
1801
1780
  raise e
1802
1781
 
1803
1782
  def do_add_column_expr(self) -> None:
@@ -1816,7 +1795,7 @@ class DataFrameTable(DataTable):
1816
1795
  cidx, new_col_name, expr = result
1817
1796
 
1818
1797
  # Add to history
1819
- 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)
1820
1799
 
1821
1800
  try:
1822
1801
  # Create the column
@@ -1839,7 +1818,9 @@ class DataFrameTable(DataTable):
1839
1818
 
1840
1819
  # self.notify(f"Added column [$success]{col_name}[/]", title="Add Column")
1841
1820
  except Exception as e:
1842
- 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
+ )
1843
1824
  self.log(f"Error adding column `{new_col_name}`: {str(e)}")
1844
1825
 
1845
1826
  def do_add_link_column(self) -> None:
@@ -1868,7 +1849,7 @@ class DataFrameTable(DataTable):
1868
1849
  cidx, new_col_name, link_template = result
1869
1850
 
1870
1851
  self.add_history(
1871
- 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
1872
1853
  )
1873
1854
 
1874
1855
  try:
@@ -1904,7 +1885,9 @@ class DataFrameTable(DataTable):
1904
1885
  self.notify(f"Added link column [$success]{new_col_name}[/]. Use Ctrl/Cmd click to open.", title="Add Link")
1905
1886
 
1906
1887
  except Exception as e:
1907
- 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
+ )
1908
1891
  self.log(f"Error adding link column: {str(e)}")
1909
1892
 
1910
1893
  def do_delete_column(self, more: str = None) -> None:
@@ -2009,7 +1992,7 @@ class DataFrameTable(DataTable):
2009
1992
  # Move cursor to the new duplicated column
2010
1993
  self.move_cursor(column=col_idx + 1)
2011
1994
 
2012
- # 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")
2013
1996
 
2014
1997
  def do_delete_row(self, more: str = None) -> None:
2015
1998
  """Delete rows from the table and dataframe.
@@ -2056,7 +2039,7 @@ class DataFrameTable(DataTable):
2056
2039
  try:
2057
2040
  df = self.df.with_row_index(RIDX).filter(predicates)
2058
2041
  except Exception as e:
2059
- 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)
2060
2043
  self.histories.pop() # Remove last history entry
2061
2044
  return
2062
2045
 
@@ -2075,7 +2058,7 @@ class DataFrameTable(DataTable):
2075
2058
 
2076
2059
  deleted_count = old_count - len(self.df)
2077
2060
  if deleted_count > 0:
2078
- self.notify(f"Deleted [$accent]{deleted_count}[/] row(s)", title="Delete")
2061
+ self.notify(f"Deleted [$success]{deleted_count}[/] row(s)", title="Delete")
2079
2062
 
2080
2063
  def do_duplicate_row(self) -> None:
2081
2064
  """Duplicate the currently selected row, inserting it right after the current row."""
@@ -2147,7 +2130,8 @@ class DataFrameTable(DataTable):
2147
2130
 
2148
2131
  # Add to history
2149
2132
  self.add_history(
2150
- 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,
2151
2135
  )
2152
2136
 
2153
2137
  # Swap columns in the table's internal column locations
@@ -2202,7 +2186,7 @@ class DataFrameTable(DataTable):
2202
2186
 
2203
2187
  # Add to history
2204
2188
  self.add_history(
2205
- 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}[/])",
2206
2190
  dirty=True,
2207
2191
  )
2208
2192
 
@@ -2254,12 +2238,12 @@ class DataFrameTable(DataTable):
2254
2238
  try:
2255
2239
  target_dtype = eval(dtype)
2256
2240
  except Exception:
2257
- 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)
2258
2242
  return
2259
2243
 
2260
2244
  if current_dtype == target_dtype:
2261
2245
  self.notify(
2262
- 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}[/]",
2263
2247
  title="Cast",
2264
2248
  severity="warning",
2265
2249
  )
@@ -2267,7 +2251,7 @@ class DataFrameTable(DataTable):
2267
2251
 
2268
2252
  # Add to history
2269
2253
  self.add_history(
2270
- 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}[/]",
2271
2255
  dirty=True,
2272
2256
  )
2273
2257
 
@@ -2278,12 +2262,13 @@ class DataFrameTable(DataTable):
2278
2262
  # Recreate table for display
2279
2263
  self.setup_table()
2280
2264
 
2281
- 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")
2282
2266
  except Exception as e:
2283
2267
  self.notify(
2284
- f"Error casting column [$accent]{col_name}[/] to [$error]{target_dtype}[/]",
2268
+ f"Error casting column [$error]{col_name}[/] to [$accent]{target_dtype}[/]",
2285
2269
  title="Cast",
2286
2270
  severity="error",
2271
+ timeout=10,
2287
2272
  )
2288
2273
  self.log(f"Error casting column `{col_name}`: {str(e)}")
2289
2274
 
@@ -2326,7 +2311,9 @@ class DataFrameTable(DataTable):
2326
2311
  try:
2327
2312
  expr = validate_expr(term, self.df.columns, cidx)
2328
2313
  except Exception as e:
2329
- 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
+ )
2330
2317
  self.log(f"Error validating expression `{term}`: {str(e)}")
2331
2318
  return
2332
2319
 
@@ -2350,42 +2337,42 @@ class DataFrameTable(DataTable):
2350
2337
  term = f"(?i){term}"
2351
2338
  expr = pl.col(col_name).cast(pl.String).str.contains(term)
2352
2339
  self.notify(
2353
- f"Error converting [$accent]{term}[/] to [$error]{dtype}[/]. Cast to string.",
2340
+ f"Error converting [$error]{term}[/] to [$accent]{dtype}[/]. Cast to string.",
2354
2341
  title="Search",
2355
2342
  severity="warning",
2356
2343
  )
2357
2344
 
2358
2345
  # Lazyframe for filtering
2359
2346
  lf = self.df.lazy().with_row_index(RIDX)
2360
- if False in self.visible_rows:
2347
+ if self.has_hidden_rows:
2361
2348
  lf = lf.filter(self.visible_rows)
2362
2349
 
2363
2350
  # Apply filter to get matched row indices
2364
2351
  try:
2365
2352
  matches = set(lf.filter(expr).select(RIDX).collect().to_series().to_list())
2366
2353
  except Exception as e:
2367
- 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)
2368
2355
  self.log(f"Error applying search filter `{term}`: {str(e)}")
2369
2356
  return
2370
2357
 
2371
2358
  match_count = len(matches)
2372
2359
  if match_count == 0:
2373
2360
  self.notify(
2374
- 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.",
2375
2362
  title="Search",
2376
2363
  severity="warning",
2377
2364
  )
2378
2365
  return
2379
2366
 
2380
2367
  # Add to history
2381
- 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}[/]")
2382
2369
 
2383
2370
  # Update selected rows to include new matches
2384
2371
  for m in matches:
2385
2372
  self.selected_rows[m] = True
2386
2373
 
2387
2374
  # Show notification immediately, then start highlighting
2388
- 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")
2389
2376
 
2390
2377
  # Recreate table for display
2391
2378
  self.setup_table()
@@ -2412,7 +2399,7 @@ class DataFrameTable(DataTable):
2412
2399
 
2413
2400
  # Lazyframe for filtering
2414
2401
  lf = self.df.lazy().with_row_index(RIDX)
2415
- if False in self.visible_rows:
2402
+ if self.has_hidden_rows:
2416
2403
  lf = lf.filter(self.visible_rows)
2417
2404
 
2418
2405
  # Determine which columns to search: single column or all columns
@@ -2430,7 +2417,9 @@ class DataFrameTable(DataTable):
2430
2417
  try:
2431
2418
  expr = validate_expr(term, self.df.columns, col_idx)
2432
2419
  except Exception as e:
2433
- 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
+ )
2434
2423
  self.log(f"Error validating expression `{term}`: {str(e)}")
2435
2424
  return matches
2436
2425
  else:
@@ -2444,7 +2433,7 @@ class DataFrameTable(DataTable):
2444
2433
  try:
2445
2434
  matched_ridxs = lf.filter(expr).select(RIDX).collect().to_series().to_list()
2446
2435
  except Exception as e:
2447
- 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)
2448
2437
  self.log(f"Error applying filter: {str(e)}")
2449
2438
  return matches
2450
2439
 
@@ -2495,27 +2484,27 @@ class DataFrameTable(DataTable):
2495
2484
  try:
2496
2485
  matches = self.find_matches(term, cidx, match_nocase, match_whole)
2497
2486
  except Exception as e:
2498
- 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)
2499
2488
  self.log(f"Error finding matches for `{term}`: {str(e)}")
2500
2489
  return
2501
2490
 
2502
2491
  if not matches:
2503
2492
  self.notify(
2504
- 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.",
2505
2494
  title="Find",
2506
2495
  severity="warning",
2507
2496
  )
2508
2497
  return
2509
2498
 
2510
2499
  # Add to history
2511
- 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}[/]")
2512
2501
 
2513
2502
  # Add to matches and count total
2514
2503
  match_count = sum(len(col_idxs) for col_idxs in matches.values())
2515
2504
  for ridx, col_idxs in matches.items():
2516
2505
  self.matches[ridx].update(col_idxs)
2517
2506
 
2518
- 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")
2519
2508
 
2520
2509
  # Recreate table for display
2521
2510
  self.setup_table()
@@ -2529,13 +2518,13 @@ class DataFrameTable(DataTable):
2529
2518
  try:
2530
2519
  matches = self.find_matches(term, cidx=None, match_nocase=match_nocase, match_whole=match_whole)
2531
2520
  except Exception as e:
2532
- 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)
2533
2522
  self.log(f"Error finding matches for `{term}`: {str(e)}")
2534
2523
  return
2535
2524
 
2536
2525
  if not matches:
2537
2526
  self.notify(
2538
- 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.",
2539
2528
  title="Global Find",
2540
2529
  severity="warning",
2541
2530
  )
@@ -2550,7 +2539,7 @@ class DataFrameTable(DataTable):
2550
2539
  self.matches[ridx].update(col_idxs)
2551
2540
 
2552
2541
  self.notify(
2553
- 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"
2554
2543
  )
2555
2544
 
2556
2545
  # Recreate table for display
@@ -2700,7 +2689,7 @@ class DataFrameTable(DataTable):
2700
2689
 
2701
2690
  # Add to history
2702
2691
  self.add_history(
2703
- 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}[/]"
2704
2693
  )
2705
2694
 
2706
2695
  # Update matches
@@ -2738,9 +2727,10 @@ class DataFrameTable(DataTable):
2738
2727
 
2739
2728
  except Exception as e:
2740
2729
  self.notify(
2741
- f"Error replacing [$accent]{term_find}[/] with [$error]{term_replace}[/]",
2730
+ f"Error replacing [$error]{term_find}[/] with [$accent]{term_replace}[/]",
2742
2731
  title="Replace",
2743
2732
  severity="error",
2733
+ timeout=10,
2744
2734
  )
2745
2735
  self.log(f"Error replacing `{term_find}` with `{term_replace}`: {str(e)}")
2746
2736
 
@@ -2816,7 +2806,7 @@ class DataFrameTable(DataTable):
2816
2806
 
2817
2807
  col_name = "all columns" if state.cidx is None else self.df.columns[state.cidx]
2818
2808
  self.notify(
2819
- 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}[/]",
2820
2810
  title="Replace",
2821
2811
  )
2822
2812
 
@@ -2827,9 +2817,10 @@ class DataFrameTable(DataTable):
2827
2817
  self.show_next_replace_confirmation()
2828
2818
  except Exception as e:
2829
2819
  self.notify(
2830
- f"Error replacing [$accent]{term_find}[/] with [$error]{term_replace}[/]",
2820
+ f"Error replacing [$error]{term_find}[/] with [$accent]{term_replace}[/]",
2831
2821
  title="Replace",
2832
2822
  severity="error",
2823
+ timeout=10,
2833
2824
  )
2834
2825
  self.log(f"Error in interactive replace: {str(e)}")
2835
2826
 
@@ -2839,7 +2830,7 @@ class DataFrameTable(DataTable):
2839
2830
  if state.done:
2840
2831
  # All done - show final notification
2841
2832
  col_name = "all columns" if state.cidx is None else self.df.columns[state.cidx]
2842
- 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}[/]"
2843
2834
  if state.skipped_occurrence > 0:
2844
2835
  msg += f", [$warning]{state.skipped_occurrence}[/] skipped"
2845
2836
  self.notify(msg, title="Replace")
@@ -2938,7 +2929,7 @@ class DataFrameTable(DataTable):
2938
2929
  # Add to history
2939
2930
  self.add_history("Toggled row selection")
2940
2931
 
2941
- if False in self.visible_rows:
2932
+ if self.has_hidden_rows:
2942
2933
  # Some rows are hidden - invert only selected visible rows and clear selections for hidden rows
2943
2934
  for i in range(len(self.selected_rows)):
2944
2935
  if self.visible_rows[i]:
@@ -2951,7 +2942,7 @@ class DataFrameTable(DataTable):
2951
2942
 
2952
2943
  # Check if we're highlighting or un-highlighting
2953
2944
  if new_selected_count := self.selected_rows.count(True):
2954
- 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")
2955
2946
 
2956
2947
  # Recreate table for display
2957
2948
  self.setup_table()
@@ -3001,21 +2992,31 @@ class DataFrameTable(DataTable):
3001
2992
  # Recreate table for display
3002
2993
  self.setup_table()
3003
2994
 
3004
- 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")
3005
2996
 
3006
2997
  # Filter & View
3007
2998
  def do_filter_rows(self) -> None:
3008
- """Keep only the rows with selections and matches, and remove others."""
3009
- if not any(self.selected_rows) and not self.matches:
3010
- self.notify("No rows to filter", title="Filter", severity="warning")
3011
- 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)
3012
3011
 
3013
- filter_expr = [
3014
- True if (selected or ridx in self.matches) else False for ridx, selected in enumerate(self.selected_rows)
3015
- ]
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
3016
3017
 
3017
3018
  # Add to history
3018
- self.add_history("Filtered to selections and matches", dirty=True)
3019
+ self.add_history(message, dirty=True)
3019
3020
 
3020
3021
  # Apply filter to dataframe with row indices
3021
3022
  df_filtered = self.df.with_row_index(RIDX).filter(filter_expr)
@@ -3026,10 +3027,7 @@ class DataFrameTable(DataTable):
3026
3027
  # Recreate table for display
3027
3028
  self.setup_table()
3028
3029
 
3029
- self.notify(
3030
- f"Filtered rows with selections or matches and removed others. Now showing [$accent]{len(self.df)}[/] rows",
3031
- title="Filter",
3032
- )
3030
+ self.notify(f"{message}. Now showing [$success]{len(self.df)}[/] rows", title="Filter")
3033
3031
 
3034
3032
  def do_view_rows(self) -> None:
3035
3033
  """View rows.
@@ -3039,6 +3037,7 @@ class DataFrameTable(DataTable):
3039
3037
  """
3040
3038
 
3041
3039
  cidx = self.cursor_col_idx
3040
+ col_name = self.df.columns[cidx]
3042
3041
 
3043
3042
  # If there are rows with selections or matches, use those
3044
3043
  if any(self.selected_rows) or self.matches:
@@ -3049,7 +3048,7 @@ class DataFrameTable(DataTable):
3049
3048
  else:
3050
3049
  ridx = self.cursor_row_idx
3051
3050
  value = self.df.item(ridx, cidx)
3052
- term = NULL if value is None else str(value)
3051
+ term = pl.col(col_name).is_null() if value is None else pl.col(col_name) == value
3053
3052
 
3054
3053
  self.view_rows((term, cidx, False, True))
3055
3054
 
@@ -3073,17 +3072,22 @@ class DataFrameTable(DataTable):
3073
3072
 
3074
3073
  col_name = self.df.columns[cidx]
3075
3074
 
3076
- if term == NULL:
3077
- 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)
3078
3079
  elif isinstance(term, (list, pl.Series)):
3079
- # Support for list of booleans (selected rows)
3080
3080
  expr = term
3081
+ elif term == NULL:
3082
+ expr = pl.col(col_name).is_null()
3081
3083
  elif tentative_expr(term):
3082
- # Support for polars expressions
3084
+ # Support for polars expression in string form
3083
3085
  try:
3084
3086
  expr = validate_expr(term, self.df.columns, cidx)
3085
3087
  except Exception as e:
3086
- 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
+ )
3087
3091
  self.log(f"Error validating expression `{term}`: {str(e)}")
3088
3092
  return
3089
3093
  else:
@@ -3112,7 +3116,7 @@ class DataFrameTable(DataTable):
3112
3116
  lf = self.df.lazy().with_row_index(RIDX)
3113
3117
 
3114
3118
  # Apply existing visibility filter first
3115
- if False in self.visible_rows:
3119
+ if self.has_hidden_rows:
3116
3120
  lf = lf.filter(self.visible_rows)
3117
3121
 
3118
3122
  expr_str = "boolean list or series" if isinstance(expr, (list, pl.Series)) else str(expr)
@@ -3122,7 +3126,7 @@ class DataFrameTable(DataTable):
3122
3126
  df_filtered = lf.filter(expr).collect()
3123
3127
  except Exception as e:
3124
3128
  self.histories.pop() # Remove last history entry
3125
- 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)
3126
3130
  self.log(f"Error applying filter `{expr_str}`: {str(e)}")
3127
3131
  return
3128
3132
 
@@ -3144,7 +3148,7 @@ class DataFrameTable(DataTable):
3144
3148
  # Recreate table for display
3145
3149
  self.setup_table()
3146
3150
 
3147
- 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")
3148
3152
 
3149
3153
  # Copy & Save
3150
3154
  def do_copy_to_clipboard(self, content: str, message: str) -> None:
@@ -3168,7 +3172,7 @@ class DataFrameTable(DataTable):
3168
3172
  )
3169
3173
  self.notify(message, title="Clipboard")
3170
3174
  except FileNotFoundError:
3171
- self.notify("Error copying to clipboard", title="Clipboard", severity="error")
3175
+ self.notify("Error copying to clipboard", title="Clipboard", severity="error", timeout=10)
3172
3176
 
3173
3177
  def do_save_to_file(
3174
3178
  self, title: str = "Save to File", all_tabs: bool | None = None, task_after_save: str | None = None
@@ -3221,24 +3225,39 @@ class DataFrameTable(DataTable):
3221
3225
  """Actually save the dataframe to a file."""
3222
3226
  filepath = Path(filename)
3223
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"
3224
3239
 
3225
3240
  # Add to history
3226
3241
  self.add_history(f"Saved dataframe to [$success]{filename}[/]")
3227
3242
 
3228
3243
  try:
3229
- if ext in (".xlsx", ".xls"):
3230
- self.save_excel(filename)
3231
- elif ext in (".tsv", ".tab"):
3244
+ if fmt == "csv":
3245
+ self.df.write_csv(filename)
3246
+ elif fmt in ("tsv", "tab"):
3232
3247
  self.df.write_csv(filename, separator="\t")
3233
- elif ext == ".json":
3248
+ elif fmt in ("xlsx", "xls"):
3249
+ self.save_excel(filename)
3250
+ elif fmt == "json":
3234
3251
  self.df.write_json(filename)
3235
- elif ext == ".parquet":
3252
+ elif fmt == "ndjson":
3253
+ self.df.write_ndjson(filename)
3254
+ elif fmt == "parquet":
3236
3255
  self.df.write_parquet(filename)
3237
- else:
3256
+ else: # Fallback to CSV
3238
3257
  self.df.write_csv(filename)
3239
3258
 
3240
- self.dataframe = self.df # Update original dataframe
3241
- self.filename = filename # Update current filename
3259
+ # Update current filename
3260
+ self.filename = filename
3242
3261
 
3243
3262
  # Reset dirty flag after save
3244
3263
  if self._all_tabs:
@@ -3260,7 +3279,7 @@ class DataFrameTable(DataTable):
3260
3279
  self.notify(f"Saved current tab to [$success]{filename}[/]", title="Save to File")
3261
3280
 
3262
3281
  except Exception as e:
3263
- 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)
3264
3283
  self.log(f"Error saving file `{filename}`: {str(e)}")
3265
3284
 
3266
3285
  def save_excel(self, filename: str) -> None:
@@ -3329,7 +3348,7 @@ class DataFrameTable(DataTable):
3329
3348
  # Execute the SQL query
3330
3349
  try:
3331
3350
  lf = self.df.lazy().with_row_index(RIDX)
3332
- if False in self.visible_rows:
3351
+ if self.has_hidden_rows:
3333
3352
  lf = lf.filter(self.visible_rows)
3334
3353
 
3335
3354
  df_filtered = lf.sql(sql).collect()
@@ -3341,7 +3360,7 @@ class DataFrameTable(DataTable):
3341
3360
  return
3342
3361
 
3343
3362
  # Add to history
3344
- 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)
3345
3364
 
3346
3365
  if view:
3347
3366
  # Just view - do not modify the dataframe