dataframe-textual 2.0.1__py3-none-any.whl → 2.2.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/__main__.py +4 -0
- dataframe_textual/common.py +4 -0
- dataframe_textual/data_frame_table.py +73 -32
- dataframe_textual/data_frame_viewer.py +21 -2
- dataframe_textual/table_screen.py +1 -1
- {dataframe_textual-2.0.1.dist-info → dataframe_textual-2.2.0.dist-info}/METADATA +19 -18
- dataframe_textual-2.2.0.dist-info/RECORD +14 -0
- dataframe_textual-2.0.1.dist-info/RECORD +0 -14
- {dataframe_textual-2.0.1.dist-info → dataframe_textual-2.2.0.dist-info}/WHEEL +0 -0
- {dataframe_textual-2.0.1.dist-info → dataframe_textual-2.2.0.dist-info}/entry_points.txt +0 -0
- {dataframe_textual-2.0.1.dist-info → dataframe_textual-2.2.0.dist-info}/licenses/LICENSE +0 -0
dataframe_textual/__main__.py
CHANGED
|
@@ -46,6 +46,9 @@ def cli() -> argparse.Namespace:
|
|
|
46
46
|
parser.add_argument(
|
|
47
47
|
"-I", "--no-inference", action="store_true", help="Do not infer data types when reading CSV/TSV"
|
|
48
48
|
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"-t", "--truncate-ragged-lines", action="store_true", help="Truncate ragged lines when reading CSV/TSV"
|
|
51
|
+
)
|
|
49
52
|
parser.add_argument("-E", "--ignore-errors", action="store_true", help="Ignore errors when reading CSV/TSV")
|
|
50
53
|
parser.add_argument(
|
|
51
54
|
"-c", "--comment-prefix", nargs="?", const="#", help="Comment lines are skipped when reading CSV/TSV"
|
|
@@ -94,6 +97,7 @@ def main() -> None:
|
|
|
94
97
|
skip_rows_after_header=args.skip_rows_after_header,
|
|
95
98
|
null_values=args.null,
|
|
96
99
|
ignore_errors=args.ignore_errors,
|
|
100
|
+
truncate_ragged_lines=args.truncate_ragged_lines,
|
|
97
101
|
)
|
|
98
102
|
app = DataFrameViewer(*sources)
|
|
99
103
|
app.run()
|
dataframe_textual/common.py
CHANGED
|
@@ -484,6 +484,7 @@ def load_dataframe(
|
|
|
484
484
|
skip_rows_after_header: int = 0,
|
|
485
485
|
null_values: list[str] | None = None,
|
|
486
486
|
ignore_errors: bool = False,
|
|
487
|
+
truncate_ragged_lines: bool = False,
|
|
487
488
|
) -> list[Source]:
|
|
488
489
|
"""Load DataFrames from file specifications.
|
|
489
490
|
|
|
@@ -548,6 +549,7 @@ def load_dataframe(
|
|
|
548
549
|
skip_rows_after_header=skip_rows_after_header,
|
|
549
550
|
null_values=null_values,
|
|
550
551
|
ignore_errors=ignore_errors,
|
|
552
|
+
truncate_ragged_lines=truncate_ragged_lines,
|
|
551
553
|
)
|
|
552
554
|
)
|
|
553
555
|
|
|
@@ -624,6 +626,7 @@ def load_file(
|
|
|
624
626
|
schema_overrides: dict[str, pl.DataType] | None = None,
|
|
625
627
|
null_values: list[str] | None = None,
|
|
626
628
|
ignore_errors: bool = False,
|
|
629
|
+
truncate_ragged_lines: bool = False,
|
|
627
630
|
) -> list[Source]:
|
|
628
631
|
"""Load a single file.
|
|
629
632
|
|
|
@@ -678,6 +681,7 @@ def load_file(
|
|
|
678
681
|
schema_overrides=schema_overrides,
|
|
679
682
|
null_values=null_values,
|
|
680
683
|
ignore_errors=ignore_errors,
|
|
684
|
+
truncate_ragged_lines=truncate_ragged_lines,
|
|
681
685
|
)
|
|
682
686
|
data.append(Source(lf, filename, filepath.stem))
|
|
683
687
|
elif file_format in ("xlsx", "xls", "excel"):
|
|
@@ -20,6 +20,7 @@ from textual.widgets._data_table import (
|
|
|
20
20
|
CellDoesNotExist,
|
|
21
21
|
CellKey,
|
|
22
22
|
CellType,
|
|
23
|
+
Column,
|
|
23
24
|
ColumnKey,
|
|
24
25
|
CursorType,
|
|
25
26
|
DuplicateKey,
|
|
@@ -79,14 +80,13 @@ class History:
|
|
|
79
80
|
df: pl.DataFrame
|
|
80
81
|
df_view: pl.DataFrame | None
|
|
81
82
|
filename: str
|
|
82
|
-
loaded_rows: int
|
|
83
83
|
hidden_columns: set[str]
|
|
84
84
|
selected_rows: set[int]
|
|
85
85
|
sorted_columns: dict[str, bool] # col_name -> descending
|
|
86
|
+
matches: dict[int, set[str]] # RID -> set of col names
|
|
86
87
|
fixed_rows: int
|
|
87
88
|
fixed_columns: int
|
|
88
89
|
cursor_coordinate: Coordinate
|
|
89
|
-
matches: dict[int, set[str]] # RID -> set of col names
|
|
90
90
|
dirty: bool = False # Whether this history state has unsaved changes
|
|
91
91
|
|
|
92
92
|
|
|
@@ -110,6 +110,20 @@ class ReplaceState:
|
|
|
110
110
|
done: bool = False # Whether the replace operation is complete
|
|
111
111
|
|
|
112
112
|
|
|
113
|
+
def add_rid_column(df: pl.DataFrame) -> pl.DataFrame:
|
|
114
|
+
"""Add internal row index as last column to the dataframe if not already present.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
df: The Polars DataFrame to modify.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
The modified DataFrame with the internal row index column added.
|
|
121
|
+
"""
|
|
122
|
+
if RID not in df.columns:
|
|
123
|
+
df = df.lazy().with_row_index(RID).select(pl.exclude(RID), RID).collect()
|
|
124
|
+
return df
|
|
125
|
+
|
|
126
|
+
|
|
113
127
|
class DataFrameTable(DataTable):
|
|
114
128
|
"""Custom DataTable to highlight row/column labels based on cursor position."""
|
|
115
129
|
|
|
@@ -141,7 +155,7 @@ class DataFrameTable(DataTable):
|
|
|
141
155
|
- **M** - 📋 Show column metadata (ID, name, type)
|
|
142
156
|
- **h** - 👁️ Hide current column
|
|
143
157
|
- **H** - 👀 Show all hidden rows/columns
|
|
144
|
-
- **_** - 📏
|
|
158
|
+
- **_** - 📏 Toggle column full width
|
|
145
159
|
- **z** - 📌 Freeze rows and columns
|
|
146
160
|
- **~** - 🏷️ Toggle row labels
|
|
147
161
|
- **,** - 🔢 Toggle thousand separator for numeric display
|
|
@@ -321,7 +335,7 @@ class DataFrameTable(DataTable):
|
|
|
321
335
|
super().__init__(**kwargs)
|
|
322
336
|
|
|
323
337
|
# DataFrame state
|
|
324
|
-
self.dataframe = df
|
|
338
|
+
self.dataframe = add_rid_column(df) # Original dataframe
|
|
325
339
|
self.df = self.dataframe # Internal/working dataframe
|
|
326
340
|
self.filename = filename or "untitled.csv" # Current filename
|
|
327
341
|
self.tabname = tabname or Path(filename).stem # Tab name
|
|
@@ -352,6 +366,9 @@ class DataFrameTable(DataTable):
|
|
|
352
366
|
# Whether to use thousand separator for numeric display
|
|
353
367
|
self.thousand_separator = False
|
|
354
368
|
|
|
369
|
+
# Set of columns expanded to full width
|
|
370
|
+
self.expanded_columns: set[str] = set()
|
|
371
|
+
|
|
355
372
|
# Whether to show internal row index column
|
|
356
373
|
self.show_rid = False
|
|
357
374
|
|
|
@@ -1036,7 +1053,7 @@ class DataFrameTable(DataTable):
|
|
|
1036
1053
|
Returns:
|
|
1037
1054
|
dict[str, int]: Mapping of column name to width (None for auto-sizing columns).
|
|
1038
1055
|
"""
|
|
1039
|
-
|
|
1056
|
+
col_widths, col_label_widths = {}, {}
|
|
1040
1057
|
|
|
1041
1058
|
# Get available width for the table (with some padding for borders/scrollbar)
|
|
1042
1059
|
available_width = self.scrollable_content_region.width
|
|
@@ -1046,7 +1063,7 @@ class DataFrameTable(DataTable):
|
|
|
1046
1063
|
|
|
1047
1064
|
# No string columns, let TextualDataTable auto-size all columns
|
|
1048
1065
|
if not string_cols:
|
|
1049
|
-
return
|
|
1066
|
+
return col_widths
|
|
1050
1067
|
|
|
1051
1068
|
# Sample a reasonable number of rows to calculate widths (don't scan entire dataframe)
|
|
1052
1069
|
sample_size = min(self.BATCH_SIZE, len(self.df))
|
|
@@ -1060,7 +1077,10 @@ class DataFrameTable(DataTable):
|
|
|
1060
1077
|
# Get column label width
|
|
1061
1078
|
# Add padding for sort indicators if any
|
|
1062
1079
|
label_width = measure(self.app.console, col, 1) + 2
|
|
1063
|
-
|
|
1080
|
+
col_label_widths[col] = label_width
|
|
1081
|
+
|
|
1082
|
+
# Let Textual auto-size for non-string columns and already expanded columns
|
|
1083
|
+
if dtype != pl.String or col in self.expanded_columns:
|
|
1064
1084
|
available_width -= label_width
|
|
1065
1085
|
continue
|
|
1066
1086
|
|
|
@@ -1083,16 +1103,16 @@ class DataFrameTable(DataTable):
|
|
|
1083
1103
|
max_width = label_width
|
|
1084
1104
|
self.log(f"Error determining width for column '{col}': {e}")
|
|
1085
1105
|
|
|
1086
|
-
|
|
1106
|
+
col_widths[col] = max_width
|
|
1087
1107
|
available_width -= max_width
|
|
1088
1108
|
|
|
1089
1109
|
# If there's no more available width, auto-size remaining columns
|
|
1090
1110
|
if available_width < 0:
|
|
1091
|
-
for col in
|
|
1092
|
-
if
|
|
1093
|
-
|
|
1111
|
+
for col in col_widths:
|
|
1112
|
+
if col_widths[col] > STRING_WIDTH_CAP and col_label_widths[col] < STRING_WIDTH_CAP:
|
|
1113
|
+
col_widths[col] = STRING_WIDTH_CAP # Cap string columns
|
|
1094
1114
|
|
|
1095
|
-
return
|
|
1115
|
+
return col_widths
|
|
1096
1116
|
|
|
1097
1117
|
def setup_columns(self) -> None:
|
|
1098
1118
|
"""Clear table and setup columns.
|
|
@@ -1522,14 +1542,13 @@ class DataFrameTable(DataTable):
|
|
|
1522
1542
|
df=self.df,
|
|
1523
1543
|
df_view=self.df_view,
|
|
1524
1544
|
filename=self.filename,
|
|
1525
|
-
loaded_rows=self.loaded_rows,
|
|
1526
1545
|
hidden_columns=self.hidden_columns.copy(),
|
|
1527
1546
|
selected_rows=self.selected_rows.copy(),
|
|
1528
1547
|
sorted_columns=self.sorted_columns.copy(),
|
|
1548
|
+
matches={k: v.copy() for k, v in self.matches.items()},
|
|
1529
1549
|
fixed_rows=self.fixed_rows,
|
|
1530
1550
|
fixed_columns=self.fixed_columns,
|
|
1531
1551
|
cursor_coordinate=self.cursor_coordinate,
|
|
1532
|
-
matches={k: v.copy() for k, v in self.matches.items()},
|
|
1533
1552
|
dirty=self.dirty,
|
|
1534
1553
|
)
|
|
1535
1554
|
|
|
@@ -1542,14 +1561,13 @@ class DataFrameTable(DataTable):
|
|
|
1542
1561
|
self.df = history.df
|
|
1543
1562
|
self.df_view = history.df_view
|
|
1544
1563
|
self.filename = history.filename
|
|
1545
|
-
self.loaded_rows = history.loaded_rows
|
|
1546
1564
|
self.hidden_columns = history.hidden_columns.copy()
|
|
1547
1565
|
self.selected_rows = history.selected_rows.copy()
|
|
1548
1566
|
self.sorted_columns = history.sorted_columns.copy()
|
|
1567
|
+
self.matches = {k: v.copy() for k, v in history.matches.items()} if history.matches else defaultdict(set)
|
|
1549
1568
|
self.fixed_rows = history.fixed_rows
|
|
1550
1569
|
self.fixed_columns = history.fixed_columns
|
|
1551
1570
|
self.cursor_coordinate = history.cursor_coordinate
|
|
1552
|
-
self.matches = {k: v.copy() for k, v in history.matches.items()} if history.matches else defaultdict(set)
|
|
1553
1571
|
self.dirty = history.dirty
|
|
1554
1572
|
|
|
1555
1573
|
# Recreate table for display
|
|
@@ -1713,11 +1731,15 @@ class DataFrameTable(DataTable):
|
|
|
1713
1731
|
if dtype != pl.String:
|
|
1714
1732
|
return
|
|
1715
1733
|
|
|
1734
|
+
# The column to expand/shrink
|
|
1735
|
+
col: Column = self.columns[col_key]
|
|
1736
|
+
|
|
1716
1737
|
# Calculate the maximum width across all loaded rows
|
|
1717
|
-
|
|
1738
|
+
label_width = len(col_name) + 2 # Start with column name width + padding
|
|
1718
1739
|
|
|
1719
1740
|
try:
|
|
1720
1741
|
need_expand = False
|
|
1742
|
+
max_width = label_width
|
|
1721
1743
|
|
|
1722
1744
|
# Scan through all loaded rows that are visible to find max width
|
|
1723
1745
|
for row_idx in range(self.loaded_rows):
|
|
@@ -1731,22 +1753,28 @@ class DataFrameTable(DataTable):
|
|
|
1731
1753
|
if not need_expand:
|
|
1732
1754
|
return
|
|
1733
1755
|
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1756
|
+
if col_name in self.expanded_columns:
|
|
1757
|
+
col.width = max(label_width, STRING_WIDTH_CAP)
|
|
1758
|
+
self.expanded_columns.remove(col_name)
|
|
1759
|
+
else:
|
|
1760
|
+
self.expanded_columns.add(col_name)
|
|
1737
1761
|
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
self._require_update_dimensions = True
|
|
1741
|
-
self.refresh(layout=True)
|
|
1762
|
+
# Update the column width
|
|
1763
|
+
col.width = max_width
|
|
1742
1764
|
|
|
1743
|
-
# self.notify(f"Expanded column [$success]{col_name}[/] to width [$accent]{max_width}[/]", title="Expand")
|
|
1744
1765
|
except Exception as e:
|
|
1745
1766
|
self.notify(
|
|
1746
1767
|
f"Error expanding column [$error]{col_name}[/]", title="Expand Column", severity="error", timeout=10
|
|
1747
1768
|
)
|
|
1748
1769
|
self.log(f"Error expanding column `{col_name}`: {str(e)}")
|
|
1749
1770
|
|
|
1771
|
+
# Force a refresh
|
|
1772
|
+
self._update_count += 1
|
|
1773
|
+
self._require_update_dimensions = True
|
|
1774
|
+
self.refresh(layout=True)
|
|
1775
|
+
|
|
1776
|
+
# self.notify(f"Expanded column [$success]{col_name}[/] to width [$accent]{max_width}[/]", title="Expand")
|
|
1777
|
+
|
|
1750
1778
|
def do_toggle_rid(self) -> None:
|
|
1751
1779
|
"""Toggle display of the internal RID column."""
|
|
1752
1780
|
self.show_rid = not self.show_rid
|
|
@@ -1968,13 +1996,21 @@ class DataFrameTable(DataTable):
|
|
|
1968
1996
|
if self.df_view is not None:
|
|
1969
1997
|
# Get updated column from df for rows that exist in df_view
|
|
1970
1998
|
col_updated = f"^_{col_name}_^"
|
|
1971
|
-
|
|
1972
|
-
|
|
1999
|
+
col_exists = "^_exists_^"
|
|
2000
|
+
lf_updated = self.df.lazy().select(
|
|
2001
|
+
RID, pl.col(col_name).alias(col_updated), pl.lit(True).alias(col_exists)
|
|
2002
|
+
)
|
|
2003
|
+
# Join and use when/then/otherwise to handle all updates including NULLs
|
|
1973
2004
|
self.df_view = (
|
|
1974
2005
|
self.df_view.lazy()
|
|
1975
2006
|
.join(lf_updated, on=RID, how="left")
|
|
1976
|
-
.with_columns(
|
|
1977
|
-
|
|
2007
|
+
.with_columns(
|
|
2008
|
+
pl.when(pl.col(col_exists))
|
|
2009
|
+
.then(pl.col(col_updated))
|
|
2010
|
+
.otherwise(pl.col(col_name))
|
|
2011
|
+
.alias(col_name)
|
|
2012
|
+
)
|
|
2013
|
+
.drop(col_updated, col_exists)
|
|
1978
2014
|
.collect()
|
|
1979
2015
|
)
|
|
1980
2016
|
except Exception as e:
|
|
@@ -2272,7 +2308,12 @@ class DataFrameTable(DataTable):
|
|
|
2272
2308
|
"""Remove the currently selected column from the table."""
|
|
2273
2309
|
# Get the column to remove
|
|
2274
2310
|
col_idx = self.cursor_column
|
|
2275
|
-
|
|
2311
|
+
try:
|
|
2312
|
+
col_name = self.cursor_col_name
|
|
2313
|
+
except CellDoesNotExist:
|
|
2314
|
+
self.notify("No column to delete at the current cursor position", title="Delete Column", severity="warning")
|
|
2315
|
+
return
|
|
2316
|
+
|
|
2276
2317
|
col_key = self.cursor_col_key
|
|
2277
2318
|
|
|
2278
2319
|
col_names_to_remove = []
|
|
@@ -2337,7 +2378,7 @@ class DataFrameTable(DataTable):
|
|
|
2337
2378
|
if self.df_view is not None:
|
|
2338
2379
|
self.df_view = self.df_view.drop(col_names_to_remove)
|
|
2339
2380
|
|
|
2340
|
-
self.notify(message, title="Delete")
|
|
2381
|
+
self.notify(message, title="Delete Column")
|
|
2341
2382
|
|
|
2342
2383
|
def do_duplicate_column(self) -> None:
|
|
2343
2384
|
"""Duplicate the currently selected column, inserting it right after the current column."""
|
|
@@ -2389,7 +2430,7 @@ class DataFrameTable(DataTable):
|
|
|
2389
2430
|
# Delete all selected rows
|
|
2390
2431
|
if selected_count := len(self.selected_rows):
|
|
2391
2432
|
history_desc = f"Deleted {selected_count} selected row(s)"
|
|
2392
|
-
rids_to_delete
|
|
2433
|
+
rids_to_delete.update(self.selected_rows)
|
|
2393
2434
|
|
|
2394
2435
|
# Delete current row and those above
|
|
2395
2436
|
elif more == "above":
|
|
@@ -279,6 +279,14 @@ class DataFrameViewer(App):
|
|
|
279
279
|
def action_duplicate_tab(self) -> None:
|
|
280
280
|
"""Duplicate the currently active tab.
|
|
281
281
|
|
|
282
|
+
Creates a copy of the current tab with the same data and filename.
|
|
283
|
+
The new tab is named with '_copy' suffix and inserted after the current tab.
|
|
284
|
+
"""
|
|
285
|
+
self.do_duplicate_tab()
|
|
286
|
+
|
|
287
|
+
def do_duplicate_tab(self) -> None:
|
|
288
|
+
"""Duplicate the currently active tab.
|
|
289
|
+
|
|
282
290
|
Creates a copy of the current tab with the same data and filename.
|
|
283
291
|
The new tab is named with '_copy' suffix and inserted after the current tab.
|
|
284
292
|
"""
|
|
@@ -320,6 +328,17 @@ class DataFrameViewer(App):
|
|
|
320
328
|
Cycles through tabs by the specified offset. With offset=1, moves to next tab.
|
|
321
329
|
With offset=-1, moves to previous tab. Wraps around when reaching edges.
|
|
322
330
|
|
|
331
|
+
Args:
|
|
332
|
+
offset: Number of tabs to advance (+1 for next, -1 for previous). Defaults to 1.
|
|
333
|
+
"""
|
|
334
|
+
self.do_next_tab(offset)
|
|
335
|
+
|
|
336
|
+
def do_next_tab(self, offset: int = 1) -> None:
|
|
337
|
+
"""Switch to the next tab or previous tab.
|
|
338
|
+
|
|
339
|
+
Cycles through tabs by the specified offset. With offset=1, moves to next tab.
|
|
340
|
+
With offset=-1, moves to previous tab. Wraps around when reaching edges.
|
|
341
|
+
|
|
323
342
|
Args:
|
|
324
343
|
offset: Number of tabs to advance (+1 for next, -1 for previous). Defaults to 1.
|
|
325
344
|
"""
|
|
@@ -478,7 +497,7 @@ class DataFrameViewer(App):
|
|
|
478
497
|
"""Handle the "save before closing?" confirmation."""
|
|
479
498
|
if result:
|
|
480
499
|
# User wants to save - close after save dialog opens
|
|
481
|
-
active_table.do_save_to_file(
|
|
500
|
+
active_table.do_save_to_file(task_after_save="close_tab")
|
|
482
501
|
elif result is None:
|
|
483
502
|
# User cancelled - do nothing
|
|
484
503
|
return
|
|
@@ -549,7 +568,7 @@ class DataFrameViewer(App):
|
|
|
549
568
|
)
|
|
550
569
|
self.push_screen(
|
|
551
570
|
ConfirmScreen(
|
|
552
|
-
"Close All Tabs",
|
|
571
|
+
"Close All Tabs" if len(self.tabs) > 1 else "Close Tab",
|
|
553
572
|
label=label,
|
|
554
573
|
yes="Save",
|
|
555
574
|
maybe="Discard",
|
|
@@ -163,7 +163,7 @@ class RowDetailScreen(TableScreen):
|
|
|
163
163
|
|
|
164
164
|
# Get all columns and values from the dataframe row
|
|
165
165
|
for col, val, dtype in zip(self.df.columns, self.df.row(self.ridx), self.df.dtypes):
|
|
166
|
-
if col == RID:
|
|
166
|
+
if col in self.dftable.hidden_columns or col == RID:
|
|
167
167
|
continue # Skip RID column
|
|
168
168
|
formatted_row = []
|
|
169
169
|
formatted_row.append(col)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dataframe-textual
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Interactive terminal viewer/editor for tabular data
|
|
5
5
|
Project-URL: Homepage, https://github.com/need47/dataframe-textual
|
|
6
6
|
Project-URL: Repository, https://github.com/need47/dataframe-textual.git
|
|
@@ -247,8 +247,8 @@ zcat compressed_data.csv.gz | dv -f csv
|
|
|
247
247
|
| `Q` | Close all tabs and app (prompts to save unsaved changes) |
|
|
248
248
|
| `Ctrl+Q` | Force to quit app (regardless of unsaved changes) |
|
|
249
249
|
| `Ctrl+T` | Save current tab to file |
|
|
250
|
+
| `Ctrl+A` | Save all tabs to a Excel file |
|
|
250
251
|
| `w` | Save current tab to file (overwrite without prompt) |
|
|
251
|
-
| `Ctrl+A` | Save all tabs to file |
|
|
252
252
|
| `W` | Save all tabs to file (overwrite without prompt) |
|
|
253
253
|
| `Ctrl+D` | Duplicate current tab |
|
|
254
254
|
| `Ctrl+O` | Open file in a new tab |
|
|
@@ -271,7 +271,7 @@ zcat compressed_data.csv.gz | dv -f csv
|
|
|
271
271
|
| Key | Action |
|
|
272
272
|
|-----|--------|
|
|
273
273
|
| `g` | Jump to first row |
|
|
274
|
-
| `G` | Jump to last row
|
|
274
|
+
| `G` | Jump to last row |
|
|
275
275
|
| `↑` / `↓` | Move up/down one row |
|
|
276
276
|
| `←` / `→` | Move left/right one column |
|
|
277
277
|
| `Home` / `End` | Jump to first/last column |
|
|
@@ -295,9 +295,11 @@ zcat compressed_data.csv.gz | dv -f csv
|
|
|
295
295
|
| `F` | Show frequency distribution for current column |
|
|
296
296
|
| `s` | Show statistics for current column |
|
|
297
297
|
| `S` | Show statistics for entire dataframe |
|
|
298
|
+
| `m` | Show metadata for row count and column count |
|
|
299
|
+
| `M` | Show metadata for current column |
|
|
298
300
|
| `K` | Cycle cursor types: cell → row → column → cell |
|
|
299
301
|
| `~` | Toggle row labels |
|
|
300
|
-
| `_` (underscore) |
|
|
302
|
+
| `_` (underscore) | Toggle column full width |
|
|
301
303
|
| `z` | Freeze rows and columns |
|
|
302
304
|
| `,` | Toggle thousand separator for numeric display |
|
|
303
305
|
| `h` | Hide current column |
|
|
@@ -318,7 +320,7 @@ zcat compressed_data.csv.gz | dv -f csv
|
|
|
318
320
|
| `x` | Delete current row |
|
|
319
321
|
| `X` | Delete current row and all those below |
|
|
320
322
|
| `Ctrl+X` | Delete current row and all those above |
|
|
321
|
-
| `d` | Duplicate current column
|
|
323
|
+
| `d` | Duplicate current column |
|
|
322
324
|
| `D` | Duplicate current row |
|
|
323
325
|
|
|
324
326
|
#### Row Selection
|
|
@@ -392,7 +394,7 @@ zcat compressed_data.csv.gz | dv -f csv
|
|
|
392
394
|
| `c` | Copy current cell to clipboard |
|
|
393
395
|
| `Ctrl+C` | Copy column to clipboard |
|
|
394
396
|
| `Ctrl+R` | Copy row to clipboard (tab-separated) |
|
|
395
|
-
| `Ctrl+S` | Save
|
|
397
|
+
| `Ctrl+S` | Save to file |
|
|
396
398
|
|
|
397
399
|
## Features in Detail
|
|
398
400
|
|
|
@@ -413,8 +415,8 @@ Useful for examining wide datasets where columns don't fit well on screen.
|
|
|
413
415
|
**In the Row Detail Modal**:
|
|
414
416
|
- Press `v` to **view** all rows containing the selected column value (others hidden but preserved)
|
|
415
417
|
- Press `"` to **filter** all rows containing the selected column value (others removed)
|
|
416
|
-
- Press `{` to move to the
|
|
417
|
-
- Press `}` to move to the
|
|
418
|
+
- Press `{` to move to the previous row
|
|
419
|
+
- Press `}` to move to the next row
|
|
418
420
|
- Press `q` or `Escape` to close the modal
|
|
419
421
|
|
|
420
422
|
### 3. Row Selection
|
|
@@ -429,7 +431,7 @@ The application provides multiple modes for selecting rows (marks it for filteri
|
|
|
429
431
|
- `{` - Go to previous selected row
|
|
430
432
|
- `}` - Go to next selected row
|
|
431
433
|
|
|
432
|
-
**Advanced
|
|
434
|
+
**Advanced Options**:
|
|
433
435
|
|
|
434
436
|
When searching or finding, you can use checkboxes in the dialog to enable:
|
|
435
437
|
- **Match Nocase**: Ignore case differences
|
|
@@ -445,7 +447,7 @@ These options work with plain text searches. Use Polars regex patterns in expres
|
|
|
445
447
|
- Use `u` to undo any search or filter
|
|
446
448
|
|
|
447
449
|
### 4. Find & Replace
|
|
448
|
-
|
|
450
|
+
Find by value/expression and highlight matching cells:
|
|
449
451
|
- `/` - Find cursor value within current column (respects data type)
|
|
450
452
|
- `?` - Open dialog to search current column with expression
|
|
451
453
|
- `;` - Find cursor value across all columns
|
|
@@ -469,10 +471,10 @@ When you press `r` or `R`, enter:
|
|
|
469
471
|
|
|
470
472
|
**Replace Interactive**:
|
|
471
473
|
- Review each match one at a time (confirm, skip, or cancel)
|
|
472
|
-
- Shows progress
|
|
474
|
+
- Shows progress
|
|
473
475
|
|
|
474
476
|
**Tips:**
|
|
475
|
-
- Search are done by string value (i.e
|
|
477
|
+
- Search are done by string value (i.e., ignoring data type)
|
|
476
478
|
- Type `NULL` to replace null/missing values
|
|
477
479
|
- Use `Match Nocase` for case-insensitive matching
|
|
478
480
|
- Use `Match Whole` to avoid partial replacements
|
|
@@ -480,7 +482,7 @@ When you press `r` or `R`, enter:
|
|
|
480
482
|
|
|
481
483
|
### 5. Filter vs. View
|
|
482
484
|
|
|
483
|
-
Both operations show
|
|
485
|
+
Both operations show selected rows but with fundamentally different effects:
|
|
484
486
|
|
|
485
487
|
| Operation | Keyboard | Effect | Data Preserved |
|
|
486
488
|
|-----------|----------|--------|-----------------|
|
|
@@ -571,8 +573,8 @@ View quick metadata about your dataframe and columns to understand their structu
|
|
|
571
573
|
|
|
572
574
|
**Dataframe Metadata** (`m`):
|
|
573
575
|
- Press `m` to open a modal displaying:
|
|
574
|
-
- **
|
|
575
|
-
- **
|
|
576
|
+
- **Row** - Total number of rows in the dataframe
|
|
577
|
+
- **Column** - Total number of columns in the dataframe
|
|
576
578
|
|
|
577
579
|
**Column Metadata** (`M`):
|
|
578
580
|
- Press `M` to open a modal displaying details for all columns:
|
|
@@ -751,7 +753,6 @@ SELECT specific columns and apply WHERE conditions without writing full SQL:
|
|
|
751
753
|
#### Advanced SQL Interface (`L`)
|
|
752
754
|
Execute complete SQL queries for advanced data manipulation:
|
|
753
755
|
- Write full SQL queries with standard [SQL syntax](https://docs.pola.rs/api/python/stable/reference/sql/index.html)
|
|
754
|
-
- Support for JOINs, GROUP BY, aggregations, and more
|
|
755
756
|
- Access to all SQL capabilities for complex transformations
|
|
756
757
|
- Always use `self` as the table name
|
|
757
758
|
- Syntax highlighted
|
|
@@ -773,7 +774,7 @@ WHERE `product id` = 7
|
|
|
773
774
|
|
|
774
775
|
Copies value to system clipboard with `pbcopy` on macOS and `xclip` on Linux.
|
|
775
776
|
|
|
776
|
-
**Note
|
|
777
|
+
**Note**: may require a X server to work.
|
|
777
778
|
|
|
778
779
|
- Press `c` to copy cursor value
|
|
779
780
|
- Press `Ctrl+C` to copy column values
|
|
@@ -815,8 +816,8 @@ Manage multiple files and dataframes simultaneously with tabs.
|
|
|
815
816
|
- **`Double-click`** - Rename the tab
|
|
816
817
|
- **`Ctrl+D`** - Duplicate current tab (creates a copy with same data and state)
|
|
817
818
|
- **`Ctrl+T`** - Save current tab to file
|
|
818
|
-
- **`w`** - Save current tab to file (overwrite without prompt)
|
|
819
819
|
- **`Ctrl+A`** - Save all tabs in a single Excel file
|
|
820
|
+
- **`w`** - Save current tab to file (overwrite without prompt)
|
|
820
821
|
- **`W`** - Save all tabs to file (overwrite without prompt)
|
|
821
822
|
- **`q`** - Close current tab (closes tab, prompts to save if unsaved changes)
|
|
822
823
|
- **`Q`** - Close all tabs and exit app (prompts to save tabs with unsaved changes)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
dataframe_textual/__init__.py,sha256=E53fW1spQRA4jW9grxSqPEmoe9zofzr6twdveMbt_W8,1310
|
|
2
|
+
dataframe_textual/__main__.py,sha256=xXeUA2EqVhufPkTbvv6MOCt3_ESHBH3PsCE--07a0ww,3613
|
|
3
|
+
dataframe_textual/common.py,sha256=ZDhAT13UCgwtQborPbcHboX956A6N-m1-YvCpZHFtwY,28108
|
|
4
|
+
dataframe_textual/data_frame_help_panel.py,sha256=UEtj64XsVRdtLzuwOaITfoEQUkAfwFuvpr5Npip5WHs,3381
|
|
5
|
+
dataframe_textual/data_frame_table.py,sha256=sxs2AZ6UUmiPc6yf503Vvj3FoASgcQuDSec17pN1I80,149069
|
|
6
|
+
dataframe_textual/data_frame_viewer.py,sha256=fyrMSH2dFxc3h3HkJSaT1jBCQyoUUstGVXXPSG6wyjA,23099
|
|
7
|
+
dataframe_textual/sql_screen.py,sha256=P3j1Fv45NIKEYo9adb7NPod54FaU-djFIvCUMMHbvjY,7534
|
|
8
|
+
dataframe_textual/table_screen.py,sha256=XPzJI6FXjwnxtQSMTmluygwkYM-0-Lx3v9o-MuL6bMg,19071
|
|
9
|
+
dataframe_textual/yes_no_screen.py,sha256=NI7Zt3rETDWYiT5CH_FDy7sIWkZ7d7LquaZZbX79b2g,26400
|
|
10
|
+
dataframe_textual-2.2.0.dist-info/METADATA,sha256=yf7TJe5fHtrSlTl6nI3PkpzE80aTW-zuJKsBbZ0q0pQ,29160
|
|
11
|
+
dataframe_textual-2.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
12
|
+
dataframe_textual-2.2.0.dist-info/entry_points.txt,sha256=R_GoooOxcq6ab4RaHiVoZ4zrZJ-phMcGmlL2rwqncW8,107
|
|
13
|
+
dataframe_textual-2.2.0.dist-info/licenses/LICENSE,sha256=AVTg0gk1X-LHI-nnHlAMDQetrwuDZK4eypgSMDO46Yc,1069
|
|
14
|
+
dataframe_textual-2.2.0.dist-info/RECORD,,
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
dataframe_textual/__init__.py,sha256=E53fW1spQRA4jW9grxSqPEmoe9zofzr6twdveMbt_W8,1310
|
|
2
|
-
dataframe_textual/__main__.py,sha256=vgHjpSsHBF34UN46iMgu_EiebZUzxWwJZ_ngOU3nQvI,3412
|
|
3
|
-
dataframe_textual/common.py,sha256=gpNNY5ZePJkJPf-YoLSCHKaBpdXQ1yW2iSKYV6zZYUo,27908
|
|
4
|
-
dataframe_textual/data_frame_help_panel.py,sha256=UEtj64XsVRdtLzuwOaITfoEQUkAfwFuvpr5Npip5WHs,3381
|
|
5
|
-
dataframe_textual/data_frame_table.py,sha256=p7PXV-39IRimrzdbuDfiacK9yNQ7XXkWBFyZ4finEk8,147693
|
|
6
|
-
dataframe_textual/data_frame_viewer.py,sha256=fkiQ0OGi2rrE06VAVJuAM_9wwmqLY1AZouwEMNoDmy8,22367
|
|
7
|
-
dataframe_textual/sql_screen.py,sha256=P3j1Fv45NIKEYo9adb7NPod54FaU-djFIvCUMMHbvjY,7534
|
|
8
|
-
dataframe_textual/table_screen.py,sha256=XlVxU_haCxPoA41ZIDcwixOg341Wf35JrFwPoCTnMzE,19033
|
|
9
|
-
dataframe_textual/yes_no_screen.py,sha256=NI7Zt3rETDWYiT5CH_FDy7sIWkZ7d7LquaZZbX79b2g,26400
|
|
10
|
-
dataframe_textual-2.0.1.dist-info/METADATA,sha256=XUm9YBbqWaSjAGv8uOrAk8ss1blAjG30BLZwJn2IjwM,29306
|
|
11
|
-
dataframe_textual-2.0.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
12
|
-
dataframe_textual-2.0.1.dist-info/entry_points.txt,sha256=R_GoooOxcq6ab4RaHiVoZ4zrZJ-phMcGmlL2rwqncW8,107
|
|
13
|
-
dataframe_textual-2.0.1.dist-info/licenses/LICENSE,sha256=AVTg0gk1X-LHI-nnHlAMDQetrwuDZK4eypgSMDO46Yc,1069
|
|
14
|
-
dataframe_textual-2.0.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|