dataframe-textual 2.4.2__py3-none-any.whl → 2.9.1__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 +24 -4
- dataframe_textual/common.py +12 -4
- dataframe_textual/data_frame_table.py +166 -242
- dataframe_textual/data_frame_viewer.py +199 -61
- dataframe_textual/yes_no_screen.py +1 -7
- {dataframe_textual-2.4.2.dist-info → dataframe_textual-2.9.1.dist-info}/METADATA +28 -27
- dataframe_textual-2.9.1.dist-info/RECORD +14 -0
- dataframe_textual-2.4.2.dist-info/RECORD +0 -14
- {dataframe_textual-2.4.2.dist-info → dataframe_textual-2.9.1.dist-info}/WHEEL +0 -0
- {dataframe_textual-2.4.2.dist-info → dataframe_textual-2.9.1.dist-info}/entry_points.txt +0 -0
- {dataframe_textual-2.4.2.dist-info → dataframe_textual-2.9.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -12,10 +12,10 @@ from textual.theme import BUILTIN_THEMES
|
|
|
12
12
|
from textual.widgets import TabbedContent, TabPane
|
|
13
13
|
from textual.widgets.tabbed_content import ContentTab, ContentTabs
|
|
14
14
|
|
|
15
|
-
from .common import Source, get_next_item, load_file
|
|
15
|
+
from .common import RID, SUPPORTED_FORMATS, Source, get_next_item, load_file
|
|
16
16
|
from .data_frame_help_panel import DataFrameHelpPanel
|
|
17
17
|
from .data_frame_table import DataFrameTable
|
|
18
|
-
from .yes_no_screen import ConfirmScreen, OpenFileScreen, RenameTabScreen
|
|
18
|
+
from .yes_no_screen import ConfirmScreen, OpenFileScreen, RenameTabScreen, SaveFileScreen
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class DataFrameViewer(App):
|
|
@@ -34,7 +34,7 @@ class DataFrameViewer(App):
|
|
|
34
34
|
- **Ctrl+Q** - 🚪 Force to quit app (discards unsaved changes)
|
|
35
35
|
- **Ctrl+T** - 💾 Save current tab to file
|
|
36
36
|
- **w** - 💾 Save current tab to file (overwrite without prompt)
|
|
37
|
-
- **Ctrl+
|
|
37
|
+
- **Ctrl+S** - 💾 Save all tabs to file
|
|
38
38
|
- **W** - 💾 Save all tabs to file (overwrite without prompt)
|
|
39
39
|
- **Ctrl+D** - 📋 Duplicate current tab
|
|
40
40
|
- **Ctrl+O** - 📁 Open a file
|
|
@@ -64,7 +64,7 @@ class DataFrameViewer(App):
|
|
|
64
64
|
("f1", "toggle_help_panel", "Help"),
|
|
65
65
|
("ctrl+o", "open_file", "Open File"),
|
|
66
66
|
("ctrl+t", "save_current_tab", "Save Current Tab"),
|
|
67
|
-
("ctrl+
|
|
67
|
+
("ctrl+s", "save_all_tabs", "Save All Tabs"),
|
|
68
68
|
("w", "save_current_tab_overwrite", "Save Current Tab (overwrite)"),
|
|
69
69
|
("W", "save_all_tabs_overwrite", "Save All Tabs (overwrite)"),
|
|
70
70
|
("ctrl+d", "duplicate_tab", "Duplicate Tab"),
|
|
@@ -102,6 +102,22 @@ class DataFrameViewer(App):
|
|
|
102
102
|
self.tabs: dict[TabPane, DataFrameTable] = {}
|
|
103
103
|
self.help_panel = None
|
|
104
104
|
|
|
105
|
+
@property
|
|
106
|
+
def active_table(self) -> DataFrameTable | None:
|
|
107
|
+
"""Get the currently active DataFrameTable widget.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
The active DataFrameTable widget, or None if not found.
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
tabbed: TabbedContent = self.query_one(TabbedContent)
|
|
114
|
+
if active_pane := tabbed.active_pane:
|
|
115
|
+
return active_pane.query_one(DataFrameTable)
|
|
116
|
+
except (NoMatches, AttributeError):
|
|
117
|
+
self.notify("No active table found", title="Locate Table", severity="error", timeout=10)
|
|
118
|
+
|
|
119
|
+
return None
|
|
120
|
+
|
|
105
121
|
def compose(self) -> ComposeResult:
|
|
106
122
|
"""Compose the application widget structure.
|
|
107
123
|
|
|
@@ -151,7 +167,7 @@ class DataFrameViewer(App):
|
|
|
151
167
|
"""
|
|
152
168
|
if len(self.tabs) == 1:
|
|
153
169
|
self.query_one(ContentTabs).display = False
|
|
154
|
-
self.
|
|
170
|
+
self.active_table.focus()
|
|
155
171
|
|
|
156
172
|
def on_ready(self) -> None:
|
|
157
173
|
"""Called when the app is ready."""
|
|
@@ -201,13 +217,11 @@ class DataFrameViewer(App):
|
|
|
201
217
|
event: The tab activated event containing the activated tab pane.
|
|
202
218
|
"""
|
|
203
219
|
# Focus the table in the newly activated tab
|
|
204
|
-
if table := self.
|
|
220
|
+
if table := self.active_table:
|
|
205
221
|
table.focus()
|
|
206
|
-
else:
|
|
207
|
-
return
|
|
208
222
|
|
|
209
|
-
|
|
210
|
-
|
|
223
|
+
if table.loaded_rows == 0:
|
|
224
|
+
table.setup_table()
|
|
211
225
|
|
|
212
226
|
def action_toggle_help_panel(self) -> None:
|
|
213
227
|
"""Toggle the help panel on or off.
|
|
@@ -245,38 +259,43 @@ class DataFrameViewer(App):
|
|
|
245
259
|
self.do_close_all_tabs()
|
|
246
260
|
|
|
247
261
|
def action_save_current_tab(self) -> None:
|
|
248
|
-
"""
|
|
249
|
-
|
|
250
|
-
Opens the save dialog for the active tab's DataFrameTable to save its data.
|
|
251
|
-
"""
|
|
252
|
-
if table := self.get_active_table():
|
|
253
|
-
table.do_save_to_file(all_tabs=False)
|
|
262
|
+
"""Open a save dialog to save current tab to file."""
|
|
263
|
+
self.do_save_to_file(all_tabs=False)
|
|
254
264
|
|
|
255
265
|
def action_save_all_tabs(self) -> None:
|
|
256
|
-
"""
|
|
257
|
-
|
|
258
|
-
Iterates through all DataFrameTable widgets and opens the save dialog for each.
|
|
259
|
-
"""
|
|
260
|
-
if table := self.get_active_table():
|
|
261
|
-
table.do_save_to_file(all_tabs=True)
|
|
266
|
+
"""Open a save dialog to save all tabs to file."""
|
|
267
|
+
self.do_save_to_file(all_tabs=True)
|
|
262
268
|
|
|
263
269
|
def action_save_current_tab_overwrite(self) -> None:
|
|
264
|
-
"""Save
|
|
265
|
-
if table := self.
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
270
|
+
"""Save current tab to file, overwrite if exists."""
|
|
271
|
+
if table := self.active_table:
|
|
272
|
+
if len(self.tabs) > 1:
|
|
273
|
+
filenames = {t.filename for t in self.tabs.values()}
|
|
274
|
+
if len(filenames) > 1:
|
|
275
|
+
# Different filenames across tabs
|
|
276
|
+
filepath = Path(table.filename)
|
|
277
|
+
filename = filepath.with_stem(table.tabname)
|
|
278
|
+
else:
|
|
279
|
+
filename = table.filename
|
|
280
|
+
else:
|
|
281
|
+
filename = table.filename
|
|
282
|
+
|
|
283
|
+
self.save_to_file((filename, False, False))
|
|
269
284
|
|
|
270
285
|
def action_save_all_tabs_overwrite(self) -> None:
|
|
271
|
-
"""Save all
|
|
272
|
-
if table := self.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
286
|
+
"""Save all tabs to file, overwrite if exists."""
|
|
287
|
+
if table := self.active_table:
|
|
288
|
+
if len(self.tabs) > 1:
|
|
289
|
+
filenames = {t.filename for t in self.tabs.values()}
|
|
290
|
+
if len(filenames) > 1:
|
|
291
|
+
# Different filenames across tabs - use generic name
|
|
292
|
+
filename = "all-tabs.xlsx"
|
|
293
|
+
else:
|
|
294
|
+
filename = table.filename
|
|
276
295
|
else:
|
|
277
|
-
filename =
|
|
296
|
+
filename = table.filename
|
|
278
297
|
|
|
279
|
-
|
|
298
|
+
self.save_to_file((filename, True, False))
|
|
280
299
|
|
|
281
300
|
def action_duplicate_tab(self) -> None:
|
|
282
301
|
"""Duplicate the currently active tab.
|
|
@@ -292,7 +311,7 @@ class DataFrameViewer(App):
|
|
|
292
311
|
Creates a copy of the current tab with the same data and filename.
|
|
293
312
|
The new tab is named with '_copy' suffix and inserted after the current tab.
|
|
294
313
|
"""
|
|
295
|
-
if not (table := self.
|
|
314
|
+
if not (table := self.active_table):
|
|
296
315
|
return
|
|
297
316
|
|
|
298
317
|
# Get current tab info
|
|
@@ -364,24 +383,6 @@ class DataFrameViewer(App):
|
|
|
364
383
|
# status = "shown" if tabs.display else "hidden"
|
|
365
384
|
# self.notify(f"Tab bar [$success]{status}[/]", title="Toggle Tab Bar")
|
|
366
385
|
|
|
367
|
-
def get_active_table(self) -> DataFrameTable | None:
|
|
368
|
-
"""Get the currently active DataFrameTable widget.
|
|
369
|
-
|
|
370
|
-
Retrieves the table from the currently active tab. Returns None if no
|
|
371
|
-
table is found or an error occurs.
|
|
372
|
-
|
|
373
|
-
Returns:
|
|
374
|
-
The active DataFrameTable widget, or None if not found.
|
|
375
|
-
"""
|
|
376
|
-
try:
|
|
377
|
-
tabbed: TabbedContent = self.query_one(TabbedContent)
|
|
378
|
-
if active_pane := tabbed.active_pane:
|
|
379
|
-
return active_pane.query_one(DataFrameTable)
|
|
380
|
-
except (NoMatches, AttributeError):
|
|
381
|
-
self.notify("No active table found", title="Locate Table", severity="error", timeout=10)
|
|
382
|
-
|
|
383
|
-
return None
|
|
384
|
-
|
|
385
386
|
def get_unique_tabname(self, tab_name: str) -> str:
|
|
386
387
|
"""Generate a unique tab name based on the given base name.
|
|
387
388
|
|
|
@@ -492,17 +493,14 @@ class DataFrameViewer(App):
|
|
|
492
493
|
can be closed, the application exits instead.
|
|
493
494
|
"""
|
|
494
495
|
try:
|
|
495
|
-
if not (
|
|
496
|
-
return
|
|
497
|
-
|
|
498
|
-
if not (active_table := self.tabs.get(active_pane)):
|
|
496
|
+
if not (table := self.active_table):
|
|
499
497
|
return
|
|
500
498
|
|
|
501
499
|
def _on_save_confirm(result: bool) -> None:
|
|
502
500
|
"""Handle the "save before closing?" confirmation."""
|
|
503
501
|
if result:
|
|
504
502
|
# User wants to save - close after save dialog opens
|
|
505
|
-
|
|
503
|
+
self.do_save_to_file(all_tabs=False, task_after_save="close_tab")
|
|
506
504
|
elif result is None:
|
|
507
505
|
# User cancelled - do nothing
|
|
508
506
|
return
|
|
@@ -510,7 +508,7 @@ class DataFrameViewer(App):
|
|
|
510
508
|
# User wants to discard - close immediately
|
|
511
509
|
self.close_tab()
|
|
512
510
|
|
|
513
|
-
if
|
|
511
|
+
if table.dirty:
|
|
514
512
|
self.push_screen(
|
|
515
513
|
ConfirmScreen(
|
|
516
514
|
"Close Tab",
|
|
@@ -557,7 +555,7 @@ class DataFrameViewer(App):
|
|
|
557
555
|
|
|
558
556
|
def _save_and_quit(result: bool) -> None:
|
|
559
557
|
if result:
|
|
560
|
-
self.
|
|
558
|
+
self.do_save_to_file(all_tabs=True, task_after_save="quit_app")
|
|
561
559
|
elif result is None:
|
|
562
560
|
# User cancelled - do nothing
|
|
563
561
|
return
|
|
@@ -565,15 +563,17 @@ class DataFrameViewer(App):
|
|
|
565
563
|
# User wants to discard - quit immediately
|
|
566
564
|
self.exit()
|
|
567
565
|
|
|
566
|
+
tab_count = len(self.tabs)
|
|
568
567
|
tab_list = "\n".join(f" - [$warning]{name}[/]" for name in dirty_tabnames)
|
|
569
568
|
label = (
|
|
570
569
|
f"The following tabs have unsaved changes:\n\n{tab_list}\n\nSave all changes?"
|
|
571
570
|
if len(dirty_tabnames) > 1
|
|
572
571
|
else f"The tab [$warning]{dirty_tabnames[0]}[/] has unsaved changes.\n\nSave changes?"
|
|
573
572
|
)
|
|
573
|
+
|
|
574
574
|
self.push_screen(
|
|
575
575
|
ConfirmScreen(
|
|
576
|
-
"Close
|
|
576
|
+
f"Close {tab_count} Tabs" if tab_count > 1 else "Close Tab",
|
|
577
577
|
label=label,
|
|
578
578
|
yes="Save",
|
|
579
579
|
maybe="Discard",
|
|
@@ -628,3 +628,141 @@ class DataFrameViewer(App):
|
|
|
628
628
|
break
|
|
629
629
|
|
|
630
630
|
# self.notify(f"Renamed tab [$accent]{old_name}[/] to [$success]{new_name}[/]", title="Rename Tab")
|
|
631
|
+
|
|
632
|
+
def do_save_to_file(self, all_tabs: bool = True, task_after_save: str | None = None) -> None:
|
|
633
|
+
"""Open screen to save file."""
|
|
634
|
+
if not (table := self.active_table):
|
|
635
|
+
return
|
|
636
|
+
|
|
637
|
+
self._task_after_save = task_after_save
|
|
638
|
+
tab_count = len(self.tabs)
|
|
639
|
+
save_all = all_tabs is True and tab_count > 1
|
|
640
|
+
|
|
641
|
+
if save_all:
|
|
642
|
+
filenames = {t.filename for t in self.tabs.values()}
|
|
643
|
+
if len(filenames) > 1:
|
|
644
|
+
# Different filenames across tabs - use generic name
|
|
645
|
+
filename = "all-tabs.xlsx"
|
|
646
|
+
else:
|
|
647
|
+
filename = table.filename
|
|
648
|
+
elif tab_count == 1:
|
|
649
|
+
filename = table.filename
|
|
650
|
+
else:
|
|
651
|
+
filepath = Path(table.filename)
|
|
652
|
+
filename = str(filepath.with_stem(table.tabname))
|
|
653
|
+
|
|
654
|
+
self.push_screen(
|
|
655
|
+
SaveFileScreen(filename, save_all=save_all, tab_count=tab_count),
|
|
656
|
+
callback=self.save_to_file,
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
def save_to_file(self, result) -> None:
|
|
660
|
+
"""Handle result from SaveFileScreen."""
|
|
661
|
+
if result is None:
|
|
662
|
+
return
|
|
663
|
+
filename, save_all, overwrite_prompt = result
|
|
664
|
+
self._save_all = save_all
|
|
665
|
+
|
|
666
|
+
# Check if file exists
|
|
667
|
+
if overwrite_prompt and Path(filename).exists():
|
|
668
|
+
self._pending_filename = filename
|
|
669
|
+
self.push_screen(
|
|
670
|
+
ConfirmScreen("File already exists. Overwrite?"),
|
|
671
|
+
callback=self.confirm_overwrite,
|
|
672
|
+
)
|
|
673
|
+
else:
|
|
674
|
+
self.save_file(filename)
|
|
675
|
+
|
|
676
|
+
def confirm_overwrite(self, should_overwrite: bool) -> None:
|
|
677
|
+
"""Handle result from ConfirmScreen."""
|
|
678
|
+
if should_overwrite:
|
|
679
|
+
self.save_file(self._pending_filename)
|
|
680
|
+
else:
|
|
681
|
+
# Go back to SaveFileScreen to allow user to enter a different name
|
|
682
|
+
self.push_screen(
|
|
683
|
+
SaveFileScreen(self._pending_filename, save_all=self._save_all),
|
|
684
|
+
callback=self.save_to_file,
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
def save_file(self, filename: str) -> None:
|
|
688
|
+
"""Actually save to a file."""
|
|
689
|
+
if not (table := self.active_table):
|
|
690
|
+
return
|
|
691
|
+
|
|
692
|
+
filepath = Path(filename)
|
|
693
|
+
ext = filepath.suffix.lower()
|
|
694
|
+
if ext == ".gz":
|
|
695
|
+
ext = Path(filename).with_suffix("").suffix.lower()
|
|
696
|
+
|
|
697
|
+
fmt = ext.removeprefix(".")
|
|
698
|
+
if fmt not in SUPPORTED_FORMATS:
|
|
699
|
+
self.notify(
|
|
700
|
+
f"Unsupported file format [$success]{fmt}[/]. Use [$accent]CSV[/] as fallback. Supported formats: {', '.join(SUPPORTED_FORMATS)}",
|
|
701
|
+
title="Save to File",
|
|
702
|
+
severity="warning",
|
|
703
|
+
)
|
|
704
|
+
fmt = "csv"
|
|
705
|
+
|
|
706
|
+
df = (table.df if table.df_view is None else table.df_view).select(pl.exclude(RID))
|
|
707
|
+
try:
|
|
708
|
+
if fmt == "csv":
|
|
709
|
+
df.write_csv(filename)
|
|
710
|
+
elif fmt in ("tsv", "tab"):
|
|
711
|
+
df.write_csv(filename, separator="\t")
|
|
712
|
+
elif fmt == "psv":
|
|
713
|
+
df.write_csv(filename, separator="|")
|
|
714
|
+
elif fmt in ("xlsx", "xls"):
|
|
715
|
+
self.save_excel(filename)
|
|
716
|
+
elif fmt == "json":
|
|
717
|
+
df.write_json(filename)
|
|
718
|
+
elif fmt == "ndjson":
|
|
719
|
+
df.write_ndjson(filename)
|
|
720
|
+
elif fmt == "parquet":
|
|
721
|
+
df.write_parquet(filename)
|
|
722
|
+
else: # Fallback to CSV
|
|
723
|
+
df.write_csv(filename)
|
|
724
|
+
|
|
725
|
+
# Reset dirty flag and update filename after save
|
|
726
|
+
if self._save_all:
|
|
727
|
+
for table in self.tabs.values():
|
|
728
|
+
table.dirty = False
|
|
729
|
+
table.filename = filename
|
|
730
|
+
else:
|
|
731
|
+
table.dirty = False
|
|
732
|
+
table.filename = filename
|
|
733
|
+
|
|
734
|
+
# From ConfirmScreen callback, so notify accordingly
|
|
735
|
+
if self._save_all:
|
|
736
|
+
self.notify(f"Saved all tabs to [$success]{filename}[/]", title="Save to File")
|
|
737
|
+
else:
|
|
738
|
+
self.notify(f"Saved current tab to [$success]{filename}[/]", title="Save to File")
|
|
739
|
+
|
|
740
|
+
if hasattr(self, "_task_after_save"):
|
|
741
|
+
if self._task_after_save == "close_tab":
|
|
742
|
+
self.do_close_tab()
|
|
743
|
+
elif self._task_after_save == "quit_app":
|
|
744
|
+
self.exit()
|
|
745
|
+
|
|
746
|
+
except Exception as e:
|
|
747
|
+
self.notify(f"Error saving [$error]{filename}[/]", title="Save to File", severity="error", timeout=10)
|
|
748
|
+
self.log(f"Error saving file `{filename}`: {str(e)}")
|
|
749
|
+
|
|
750
|
+
def save_excel(self, filename: str) -> None:
|
|
751
|
+
"""Save to an Excel file."""
|
|
752
|
+
import xlsxwriter
|
|
753
|
+
|
|
754
|
+
if not self._save_all or len(self.tabs) == 1:
|
|
755
|
+
# Single tab - save directly
|
|
756
|
+
if not (table := self.active_table):
|
|
757
|
+
return
|
|
758
|
+
|
|
759
|
+
df = (table.df if table.df_view is None else table.df_view).select(pl.exclude(RID))
|
|
760
|
+
df.write_excel(filename, worksheet=table.tabname)
|
|
761
|
+
else:
|
|
762
|
+
# Multiple tabs - use xlsxwriter to create multiple sheets
|
|
763
|
+
with xlsxwriter.Workbook(filename) as wb:
|
|
764
|
+
tabs: dict[TabPane, DataFrameTable] = self.tabs
|
|
765
|
+
for table in tabs.values():
|
|
766
|
+
worksheet = wb.add_worksheet(table.tabname)
|
|
767
|
+
df = (table.df if table.df_view is None else table.df_view).select(pl.exclude(RID))
|
|
768
|
+
df.write_excel(workbook=wb, worksheet=worksheet)
|
|
@@ -449,13 +449,7 @@ class SearchScreen(YesNoScreen):
|
|
|
449
449
|
self.cidx = cidx
|
|
450
450
|
|
|
451
451
|
EXPR = f"ABC, (?i)abc, ^abc$, {NULL}, $_ > 50, $1 < $HP, $_.str.contains('sub')"
|
|
452
|
-
|
|
453
|
-
if "Search" in title:
|
|
454
|
-
col_name = df.columns[cidx]
|
|
455
|
-
col_dtype = df.dtypes[cidx]
|
|
456
|
-
label = f"{title} in [$success]{col_name}[/] ([$warning]{col_dtype}[/]) with value or Polars expression, e.g., {EXPR}"
|
|
457
|
-
else:
|
|
458
|
-
label = f"{title} by value or Polars expression, e.g., {EXPR}"
|
|
452
|
+
label = f"By value or Polars expression, e.g., {EXPR}"
|
|
459
453
|
|
|
460
454
|
super().__init__(
|
|
461
455
|
title=title,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dataframe-textual
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.9.1
|
|
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
|
|
@@ -167,7 +167,7 @@ When multiple files are opened:
|
|
|
167
167
|
## Command Line Options
|
|
168
168
|
|
|
169
169
|
```
|
|
170
|
-
usage: dv [-h] [-V] [-f {csv,json,
|
|
170
|
+
usage: dv [-h] [-V] [-f {csv,json,xlsx,xls,ndjson,psv,parquet,tsv}] [-H] [-I] [-t] [-E] [-c [COMMENT_PREFIX]] [-q [QUOTE_CHAR]] [-l SKIP_LINES] [-a SKIP_ROWS_AFTER_HEADER] [-n NULL [NULL ...]] [files ...]
|
|
171
171
|
|
|
172
172
|
Interactive terminal based viewer/editor for tabular data (e.g., CSV/Excel).
|
|
173
173
|
|
|
@@ -177,21 +177,22 @@ positional arguments:
|
|
|
177
177
|
options:
|
|
178
178
|
-h, --help show this help message and exit
|
|
179
179
|
-V, --version show program's version number and exit
|
|
180
|
-
-f, --format {csv,json,
|
|
181
|
-
Specify the format of the input files (csv,
|
|
180
|
+
-f, --format {csv,json,xlsx,xls,ndjson,psv,parquet,tsv}
|
|
181
|
+
Specify the format of the input files (csv, tsv etc.)
|
|
182
182
|
-H, --no-header Specify that input files have no header row when reading CSV/TSV
|
|
183
183
|
-I, --no-inference Do not infer data types when reading CSV/TSV
|
|
184
184
|
-t, --truncate-ragged-lines
|
|
185
185
|
Truncate ragged lines when reading CSV/TSV
|
|
186
186
|
-E, --ignore-errors Ignore errors when reading CSV/TSV
|
|
187
|
-
-c, --comment-prefix [
|
|
188
|
-
Comment lines are skipped when reading CSV/TSV
|
|
189
|
-
-q, --quote-char [
|
|
190
|
-
|
|
191
|
-
-
|
|
192
|
-
Skip lines when reading CSV/TSV
|
|
193
|
-
-
|
|
194
|
-
Skip rows after header when reading CSV/TSV
|
|
187
|
+
-c, --comment-prefix [PREFIX]
|
|
188
|
+
Comment lines starting with `PREFIX` are skipped when reading CSV/TSV
|
|
189
|
+
-q, --quote-char [C]
|
|
190
|
+
Use `C` as quote character for reading CSV/TSV
|
|
191
|
+
-L, --skip-lines N
|
|
192
|
+
Skip first N lines when reading CSV/TSV
|
|
193
|
+
-A, --skip-rows-after-header N
|
|
194
|
+
Skip N rows after header when reading CSV/TSV
|
|
195
|
+
-N, --n-rows N Stop after reading N rows from CSV/TSV
|
|
195
196
|
-n, --null NULL [NULL ...]
|
|
196
197
|
Values to interpret as null values when reading CSV/TSV
|
|
197
198
|
```
|
|
@@ -250,7 +251,7 @@ zcat compressed_data.csv.gz | dv -f csv
|
|
|
250
251
|
| `Q` | Close all tabs and app (prompts to save unsaved changes) |
|
|
251
252
|
| `Ctrl+Q` | Force to quit app (regardless of unsaved changes) |
|
|
252
253
|
| `Ctrl+T` | Save current tab to file |
|
|
253
|
-
| `Ctrl+
|
|
254
|
+
| `Ctrl+S` | Save all tabs to file |
|
|
254
255
|
| `w` | Save current tab to file (overwrite without prompt) |
|
|
255
256
|
| `W` | Save all tabs to file (overwrite without prompt) |
|
|
256
257
|
| `Ctrl+D` | Duplicate current tab |
|
|
@@ -309,12 +310,13 @@ zcat compressed_data.csv.gz | dv -f csv
|
|
|
309
310
|
| `h` | Hide current column |
|
|
310
311
|
| `H` | Show all hidden rows/columns |
|
|
311
312
|
|
|
312
|
-
####
|
|
313
|
+
#### Editing
|
|
313
314
|
|
|
314
315
|
| Key | Action |
|
|
315
316
|
|-----|--------|
|
|
316
317
|
| `Double-click` | Edit cell or rename column header |
|
|
317
|
-
| `
|
|
318
|
+
| `Delete` | Clear current cell (set to NULL) |
|
|
319
|
+
| `Shift+Delete` | Clear current column (set matching cells to NULL) |
|
|
318
320
|
| `e` | Edit current cell (respects data type) |
|
|
319
321
|
| `E` | Edit entire column with value/expression |
|
|
320
322
|
| `a` | Add empty column after current |
|
|
@@ -356,16 +358,10 @@ zcat compressed_data.csv.gz | dv -f csv
|
|
|
356
358
|
| Key | Action |
|
|
357
359
|
|-----|--------|
|
|
358
360
|
| `"` (quote) | Filter selected rows (others removed) |
|
|
361
|
+
| `.` | View rows with non-null values in current column (others hidden) |
|
|
359
362
|
| `v` | View selected rows (others hidden) |
|
|
360
363
|
| `V` | View selected by expression (others hidden) |
|
|
361
364
|
|
|
362
|
-
#### SQL Interface
|
|
363
|
-
|
|
364
|
-
| Key | Action |
|
|
365
|
-
|-----|--------|
|
|
366
|
-
| `l` | Simple SQL interface (select columns & where clause) |
|
|
367
|
-
| `L` | Advanced SQL interface (full SQL query with syntax highlight) |
|
|
368
|
-
|
|
369
365
|
#### Sorting (supporting multiple columns)
|
|
370
366
|
|
|
371
367
|
| Key | Action |
|
|
@@ -391,14 +387,20 @@ zcat compressed_data.csv.gz | dv -f csv
|
|
|
391
387
|
| `!` | Cast current column to boolean |
|
|
392
388
|
| `$` | Cast current column to string |
|
|
393
389
|
|
|
394
|
-
#### Copy
|
|
390
|
+
#### Copy
|
|
395
391
|
|
|
396
392
|
| Key | Action |
|
|
397
393
|
|-----|--------|
|
|
398
394
|
| `c` | Copy current cell to clipboard |
|
|
399
395
|
| `Ctrl+C` | Copy column to clipboard |
|
|
400
396
|
| `Ctrl+R` | Copy row to clipboard (tab-separated) |
|
|
401
|
-
|
|
397
|
+
|
|
398
|
+
#### SQL Interface
|
|
399
|
+
|
|
400
|
+
| Key | Action |
|
|
401
|
+
|-----|--------|
|
|
402
|
+
| `l` | Simple SQL interface (select columns & where clause) |
|
|
403
|
+
| `L` | Advanced SQL interface (full SQL query with syntax highlight) |
|
|
402
404
|
|
|
403
405
|
## Features in Detail
|
|
404
406
|
|
|
@@ -446,7 +448,6 @@ These options work with plain text searches. Use Polars regex patterns in expres
|
|
|
446
448
|
**Quick Tips:**
|
|
447
449
|
- Search results highlight matching rows in **red**
|
|
448
450
|
- Use expression for advanced selection (e.g., $attack > $defense)
|
|
449
|
-
- Multiple searches **accumulate** - each new search adds to the selections or matches
|
|
450
451
|
- Type-aware matching automatically converts values. Resort to string comparison if conversion fails
|
|
451
452
|
- Use `u` to undo any search or filter
|
|
452
453
|
|
|
@@ -625,7 +626,7 @@ This is useful for:
|
|
|
625
626
|
- Quick statistical summaries without external tools
|
|
626
627
|
- Comparing statistics across columns
|
|
627
628
|
|
|
628
|
-
### 11.
|
|
629
|
+
### 11. Editing
|
|
629
630
|
|
|
630
631
|
**Edit Cell** (`e` or **Double-click**):
|
|
631
632
|
- Opens modal for editing current cell
|
|
@@ -820,7 +821,7 @@ Manage multiple files and dataframes simultaneously with tabs.
|
|
|
820
821
|
- **`Double-click`** - Rename the tab
|
|
821
822
|
- **`Ctrl+D`** - Duplicate current tab (creates a copy with same data and state)
|
|
822
823
|
- **`Ctrl+T`** - Save current tab to file
|
|
823
|
-
- **`Ctrl+
|
|
824
|
+
- **`Ctrl+S`** - Save all tabs to file
|
|
824
825
|
- **`w`** - Save current tab to file (overwrite without prompt)
|
|
825
826
|
- **`W`** - Save all tabs to file (overwrite without prompt)
|
|
826
827
|
- **`q`** - Close current tab (closes tab, prompts to save if unsaved changes)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
dataframe_textual/__init__.py,sha256=E53fW1spQRA4jW9grxSqPEmoe9zofzr6twdveMbt_W8,1310
|
|
2
|
+
dataframe_textual/__main__.py,sha256=tJ6FjjV25ZQzaMdqD5XcDVRZfj8l6kgGvXyrn975rjo,3999
|
|
3
|
+
dataframe_textual/common.py,sha256=CNRdHP3N1li2dy9OsTiW-zfpzf8zcrt2fW8mmYY-YVA,29073
|
|
4
|
+
dataframe_textual/data_frame_help_panel.py,sha256=UEtj64XsVRdtLzuwOaITfoEQUkAfwFuvpr5Npip5WHs,3381
|
|
5
|
+
dataframe_textual/data_frame_table.py,sha256=PeLmedBpUvyw9ecVz15mITltb5gBMpsaPnX5PGHMpF0,148396
|
|
6
|
+
dataframe_textual/data_frame_viewer.py,sha256=_VwbCcRBgdTcrZmgS2mRwIJ-cFxOeJ55twDFvQUHMfk,28723
|
|
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=LC42DeJRIWb-PdpR3FDNvwxhnfZ6OXfU9Kxiu340BNE,26132
|
|
10
|
+
dataframe_textual-2.9.1.dist-info/METADATA,sha256=zjWgudFlnKb817dcMQe5js7KBDj8yiT0fOqlBs0yoKk,29532
|
|
11
|
+
dataframe_textual-2.9.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
12
|
+
dataframe_textual-2.9.1.dist-info/entry_points.txt,sha256=R_GoooOxcq6ab4RaHiVoZ4zrZJ-phMcGmlL2rwqncW8,107
|
|
13
|
+
dataframe_textual-2.9.1.dist-info/licenses/LICENSE,sha256=AVTg0gk1X-LHI-nnHlAMDQetrwuDZK4eypgSMDO46Yc,1069
|
|
14
|
+
dataframe_textual-2.9.1.dist-info/RECORD,,
|
|
@@ -1,14 +0,0 @@
|
|
|
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=8Fqwuco7z4p_3GDCF7Gp8SYGUz24Sncpc457FCyMMWk,28516
|
|
4
|
-
dataframe_textual/data_frame_help_panel.py,sha256=UEtj64XsVRdtLzuwOaITfoEQUkAfwFuvpr5Npip5WHs,3381
|
|
5
|
-
dataframe_textual/data_frame_table.py,sha256=4I8acfjvnOKOhzZFjhqvaaOYhrz1IZHK5WnUKrPd12M,151387
|
|
6
|
-
dataframe_textual/data_frame_viewer.py,sha256=aUjIk9BWYKyMG87PirFxR79iNLzkEcZ-I5XVnXwDEnU,23284
|
|
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.4.2.dist-info/METADATA,sha256=4EAHZULXbSWz4Eb5Mf8_VlUIOHdx9L4VY2vIhnanrLc,29482
|
|
11
|
-
dataframe_textual-2.4.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
12
|
-
dataframe_textual-2.4.2.dist-info/entry_points.txt,sha256=R_GoooOxcq6ab4RaHiVoZ4zrZJ-phMcGmlL2rwqncW8,107
|
|
13
|
-
dataframe_textual-2.4.2.dist-info/licenses/LICENSE,sha256=AVTg0gk1X-LHI-nnHlAMDQetrwuDZK4eypgSMDO46Yc,1069
|
|
14
|
-
dataframe_textual-2.4.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|