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.
- dataframe_textual/common.py +1 -1
- dataframe_textual/data_frame_table.py +197 -174
- dataframe_textual/data_frame_viewer.py +1 -5
- dataframe_textual/sql_screen.py +6 -9
- dataframe_textual/table_screen.py +56 -58
- dataframe_textual/yes_no_screen.py +2 -2
- {dataframe_textual-1.9.0.dist-info → dataframe_textual-1.12.0.dist-info}/METADATA +128 -133
- dataframe_textual-1.12.0.dist-info/RECORD +14 -0
- dataframe_textual-1.9.0.dist-info/RECORD +0 -14
- {dataframe_textual-1.9.0.dist-info → dataframe_textual-1.12.0.dist-info}/WHEEL +0 -0
- {dataframe_textual-1.9.0.dist-info → dataframe_textual-1.12.0.dist-info}/entry_points.txt +0 -0
- {dataframe_textual-1.9.0.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."""
|
|
@@ -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.
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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(
|
|
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 [$
|
|
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(
|
|
1601
|
-
|
|
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(
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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(
|
|
1744
|
-
|
|
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
|
|
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(
|
|
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 [$
|
|
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(
|
|
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 [$
|
|
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 [$
|
|
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}[/])",
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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(
|
|
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 [$
|
|
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
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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
|
|
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(
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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 [$
|
|
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
|
|
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 [$
|
|
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 [$
|
|
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
|
|
3000
|
-
|
|
3001
|
-
|
|
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
|
-
|
|
3004
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
3070
|
-
|
|
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
|
|
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(
|
|
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
|
|
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}[/]"
|
|
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 [$
|
|
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
|
|
3226
|
-
self.
|
|
3227
|
-
elif
|
|
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
|
|
3248
|
+
elif fmt in ("xlsx", "xls"):
|
|
3249
|
+
self.save_excel(filename)
|
|
3250
|
+
elif fmt == "json":
|
|
3230
3251
|
self.df.write_json(filename)
|
|
3231
|
-
elif
|
|
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
|
-
|
|
3237
|
-
self.filename = 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
|
|
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[$
|
|
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
|