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.
- dataframe_textual/common.py +1 -1
- dataframe_textual/data_frame_table.py +170 -151
- dataframe_textual/data_frame_viewer.py +1 -1
- {dataframe_textual-1.10.1.dist-info → dataframe_textual-1.12.0.dist-info}/METADATA +128 -133
- {dataframe_textual-1.10.1.dist-info → dataframe_textual-1.12.0.dist-info}/RECORD +8 -8
- {dataframe_textual-1.10.1.dist-info → dataframe_textual-1.12.0.dist-info}/WHEEL +0 -0
- {dataframe_textual-1.10.1.dist-info → dataframe_textual-1.12.0.dist-info}/entry_points.txt +0 -0
- {dataframe_textual-1.10.1.dist-info → dataframe_textual-1.12.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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(
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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(
|
|
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 [$
|
|
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(
|
|
1611
|
-
|
|
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(
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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(
|
|
1754
|
-
|
|
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
|
|
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(
|
|
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 [$
|
|
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(
|
|
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 [$
|
|
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 [$
|
|
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}[/])",
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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(
|
|
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 [$
|
|
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
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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
|
|
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(
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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
|
|
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 [$
|
|
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 [$
|
|
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
|
|
3010
|
-
|
|
3011
|
-
|
|
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
|
-
|
|
3014
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
3077
|
-
|
|
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
|
|
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(
|
|
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
|
|
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 [$
|
|
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
|
|
3230
|
-
self.
|
|
3231
|
-
elif
|
|
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
|
|
3248
|
+
elif fmt in ("xlsx", "xls"):
|
|
3249
|
+
self.save_excel(filename)
|
|
3250
|
+
elif fmt == "json":
|
|
3234
3251
|
self.df.write_json(filename)
|
|
3235
|
-
elif
|
|
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
|
-
|
|
3241
|
-
self.filename = 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
|
|
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[$
|
|
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
|