dataframe-textual 2.4.3__tar.gz → 2.5.0__tar.gz
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-2.4.3 → dataframe_textual-2.5.0}/PKG-INFO +4 -5
- {dataframe_textual-2.4.3 → dataframe_textual-2.5.0}/README.md +3 -4
- {dataframe_textual-2.4.3 → dataframe_textual-2.5.0}/pyproject.toml +1 -1
- {dataframe_textual-2.4.3 → dataframe_textual-2.5.0}/src/dataframe_textual/data_frame_table.py +3 -138
- {dataframe_textual-2.4.3 → dataframe_textual-2.5.0}/src/dataframe_textual/data_frame_viewer.py +196 -61
- {dataframe_textual-2.4.3 → dataframe_textual-2.5.0}/src/dataframe_textual/yes_no_screen.py +1 -1
- {dataframe_textual-2.4.3 → dataframe_textual-2.5.0}/uv.lock +1 -1
- {dataframe_textual-2.4.3 → dataframe_textual-2.5.0}/.gitignore +0 -0
- {dataframe_textual-2.4.3 → dataframe_textual-2.5.0}/1811.csv.gz +0 -0
- {dataframe_textual-2.4.3 → dataframe_textual-2.5.0}/LICENSE +0 -0
- {dataframe_textual-2.4.3 → dataframe_textual-2.5.0}/large_malformed.tsv.gz +0 -0
- {dataframe_textual-2.4.3 → dataframe_textual-2.5.0}/main.py +0 -0
- {dataframe_textual-2.4.3 → dataframe_textual-2.5.0}/src/dataframe_textual/__init__.py +0 -0
- {dataframe_textual-2.4.3 → dataframe_textual-2.5.0}/src/dataframe_textual/__main__.py +0 -0
- {dataframe_textual-2.4.3 → dataframe_textual-2.5.0}/src/dataframe_textual/common.py +0 -0
- {dataframe_textual-2.4.3 → dataframe_textual-2.5.0}/src/dataframe_textual/data_frame_help_panel.py +0 -0
- {dataframe_textual-2.4.3 → dataframe_textual-2.5.0}/src/dataframe_textual/sql_screen.py +0 -0
- {dataframe_textual-2.4.3 → dataframe_textual-2.5.0}/src/dataframe_textual/table_screen.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dataframe-textual
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.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
|
|
@@ -250,7 +250,7 @@ zcat compressed_data.csv.gz | dv -f csv
|
|
|
250
250
|
| `Q` | Close all tabs and app (prompts to save unsaved changes) |
|
|
251
251
|
| `Ctrl+Q` | Force to quit app (regardless of unsaved changes) |
|
|
252
252
|
| `Ctrl+T` | Save current tab to file |
|
|
253
|
-
| `Ctrl+
|
|
253
|
+
| `Ctrl+S` | Save all tabs to file |
|
|
254
254
|
| `w` | Save current tab to file (overwrite without prompt) |
|
|
255
255
|
| `W` | Save all tabs to file (overwrite without prompt) |
|
|
256
256
|
| `Ctrl+D` | Duplicate current tab |
|
|
@@ -391,14 +391,13 @@ zcat compressed_data.csv.gz | dv -f csv
|
|
|
391
391
|
| `!` | Cast current column to boolean |
|
|
392
392
|
| `$` | Cast current column to string |
|
|
393
393
|
|
|
394
|
-
#### Copy
|
|
394
|
+
#### Copy
|
|
395
395
|
|
|
396
396
|
| Key | Action |
|
|
397
397
|
|-----|--------|
|
|
398
398
|
| `c` | Copy current cell to clipboard |
|
|
399
399
|
| `Ctrl+C` | Copy column to clipboard |
|
|
400
400
|
| `Ctrl+R` | Copy row to clipboard (tab-separated) |
|
|
401
|
-
| `Ctrl+S` | Save to file |
|
|
402
401
|
|
|
403
402
|
## Features in Detail
|
|
404
403
|
|
|
@@ -820,7 +819,7 @@ Manage multiple files and dataframes simultaneously with tabs.
|
|
|
820
819
|
- **`Double-click`** - Rename the tab
|
|
821
820
|
- **`Ctrl+D`** - Duplicate current tab (creates a copy with same data and state)
|
|
822
821
|
- **`Ctrl+T`** - Save current tab to file
|
|
823
|
-
- **`Ctrl+
|
|
822
|
+
- **`Ctrl+S`** - Save all tabs to file
|
|
824
823
|
- **`w`** - Save current tab to file (overwrite without prompt)
|
|
825
824
|
- **`W`** - Save all tabs to file (overwrite without prompt)
|
|
826
825
|
- **`q`** - Close current tab (closes tab, prompts to save if unsaved changes)
|
|
@@ -211,7 +211,7 @@ zcat compressed_data.csv.gz | dv -f csv
|
|
|
211
211
|
| `Q` | Close all tabs and app (prompts to save unsaved changes) |
|
|
212
212
|
| `Ctrl+Q` | Force to quit app (regardless of unsaved changes) |
|
|
213
213
|
| `Ctrl+T` | Save current tab to file |
|
|
214
|
-
| `Ctrl+
|
|
214
|
+
| `Ctrl+S` | Save all tabs to file |
|
|
215
215
|
| `w` | Save current tab to file (overwrite without prompt) |
|
|
216
216
|
| `W` | Save all tabs to file (overwrite without prompt) |
|
|
217
217
|
| `Ctrl+D` | Duplicate current tab |
|
|
@@ -352,14 +352,13 @@ zcat compressed_data.csv.gz | dv -f csv
|
|
|
352
352
|
| `!` | Cast current column to boolean |
|
|
353
353
|
| `$` | Cast current column to string |
|
|
354
354
|
|
|
355
|
-
#### Copy
|
|
355
|
+
#### Copy
|
|
356
356
|
|
|
357
357
|
| Key | Action |
|
|
358
358
|
|-----|--------|
|
|
359
359
|
| `c` | Copy current cell to clipboard |
|
|
360
360
|
| `Ctrl+C` | Copy column to clipboard |
|
|
361
361
|
| `Ctrl+R` | Copy row to clipboard (tab-separated) |
|
|
362
|
-
| `Ctrl+S` | Save to file |
|
|
363
362
|
|
|
364
363
|
## Features in Detail
|
|
365
364
|
|
|
@@ -781,7 +780,7 @@ Manage multiple files and dataframes simultaneously with tabs.
|
|
|
781
780
|
- **`Double-click`** - Rename the tab
|
|
782
781
|
- **`Ctrl+D`** - Duplicate current tab (creates a copy with same data and state)
|
|
783
782
|
- **`Ctrl+T`** - Save current tab to file
|
|
784
|
-
- **`Ctrl+
|
|
783
|
+
- **`Ctrl+S`** - Save all tabs to file
|
|
785
784
|
- **`w`** - Save current tab to file (overwrite without prompt)
|
|
786
785
|
- **`W`** - Save all tabs to file (overwrite without prompt)
|
|
787
786
|
- **`q`** - Close current tab (closes tab, prompts to save if unsaved changes)
|
{dataframe_textual-2.4.3 → dataframe_textual-2.5.0}/src/dataframe_textual/data_frame_table.py
RENAMED
|
@@ -16,7 +16,7 @@ from textual.coordinate import Coordinate
|
|
|
16
16
|
from textual.events import Click
|
|
17
17
|
from textual.reactive import reactive
|
|
18
18
|
from textual.render import measure
|
|
19
|
-
from textual.widgets import DataTable
|
|
19
|
+
from textual.widgets import DataTable
|
|
20
20
|
from textual.widgets._data_table import (
|
|
21
21
|
CellDoesNotExist,
|
|
22
22
|
CellKey,
|
|
@@ -35,7 +35,6 @@ from .common import (
|
|
|
35
35
|
NULL_DISPLAY,
|
|
36
36
|
RID,
|
|
37
37
|
SUBSCRIPT_DIGITS,
|
|
38
|
-
SUPPORTED_FORMATS,
|
|
39
38
|
DtypeConfig,
|
|
40
39
|
format_row,
|
|
41
40
|
get_next_item,
|
|
@@ -56,7 +55,6 @@ from .yes_no_screen import (
|
|
|
56
55
|
FindReplaceScreen,
|
|
57
56
|
FreezeScreen,
|
|
58
57
|
RenameColumnScreen,
|
|
59
|
-
SaveFileScreen,
|
|
60
58
|
SearchScreen,
|
|
61
59
|
)
|
|
62
60
|
|
|
@@ -223,11 +221,10 @@ class DataFrameTable(DataTable):
|
|
|
223
221
|
- **!** - ✅ Cast column to boolean
|
|
224
222
|
- **$** - 📝 Cast column to string
|
|
225
223
|
|
|
226
|
-
## 💾 Copy
|
|
224
|
+
## 💾 Copy
|
|
227
225
|
- **c** - 📋 Copy cell to clipboard
|
|
228
226
|
- **Ctrl+c** - 📊 Copy column to clipboard
|
|
229
227
|
- **Ctrl+r** - 📝 Copy row to clipboard (tab-separated)
|
|
230
|
-
- **Ctrl+s** - 💾 Save current tab to file
|
|
231
228
|
""").strip()
|
|
232
229
|
|
|
233
230
|
# fmt: off
|
|
@@ -255,8 +252,6 @@ class DataFrameTable(DataTable):
|
|
|
255
252
|
("c", "copy_cell", "Copy cell to clipboard"),
|
|
256
253
|
("ctrl+c", "copy_column", "Copy column to clipboard"),
|
|
257
254
|
("ctrl+r", "copy_row", "Copy row to clipboard"),
|
|
258
|
-
# Save
|
|
259
|
-
("ctrl+s", "save_to_file", "Save to file"),
|
|
260
255
|
# Metadata, Detail, Frequency, and Statistics
|
|
261
256
|
("m", "metadata_shape", "Show metadata for row count and column count"),
|
|
262
257
|
("M", "metadata_column", "Show metadata for column"),
|
|
@@ -744,10 +739,6 @@ class DataFrameTable(DataTable):
|
|
|
744
739
|
"""Sort by current column in descending order."""
|
|
745
740
|
self.do_sort_by_column(descending=True)
|
|
746
741
|
|
|
747
|
-
def action_save_to_file(self) -> None:
|
|
748
|
-
"""Save the current dataframe to a file."""
|
|
749
|
-
self.do_save_to_file()
|
|
750
|
-
|
|
751
742
|
def action_show_frequency(self) -> None:
|
|
752
743
|
"""Show frequency distribution for the current column."""
|
|
753
744
|
self.do_show_frequency()
|
|
@@ -3755,7 +3746,7 @@ class DataFrameTable(DataTable):
|
|
|
3755
3746
|
|
|
3756
3747
|
self.notify(f"{message}. Now showing [$success]{len(self.df)}[/] rows.", title="Filter Rows")
|
|
3757
3748
|
|
|
3758
|
-
# Copy
|
|
3749
|
+
# Copy
|
|
3759
3750
|
def do_copy_to_clipboard(self, content: str, message: str) -> None:
|
|
3760
3751
|
"""Copy content to clipboard using pbcopy (macOS) or xclip (Linux).
|
|
3761
3752
|
|
|
@@ -3779,132 +3770,6 @@ class DataFrameTable(DataTable):
|
|
|
3779
3770
|
except FileNotFoundError:
|
|
3780
3771
|
self.notify("Error copying to clipboard", title="Copy to Clipboard", severity="error", timeout=10)
|
|
3781
3772
|
|
|
3782
|
-
def do_save_to_file(self, all_tabs: bool | None = None, task_after_save: str | None = None) -> None:
|
|
3783
|
-
"""Open screen to save file."""
|
|
3784
|
-
self._task_after_save = task_after_save
|
|
3785
|
-
tab_count = len(self.app.tabs)
|
|
3786
|
-
save_all = all_tabs is not False
|
|
3787
|
-
|
|
3788
|
-
filepath = Path(self.filename)
|
|
3789
|
-
if save_all:
|
|
3790
|
-
ext = filepath.suffix.lower()
|
|
3791
|
-
if ext in (".xlsx", ".xls"):
|
|
3792
|
-
filename = self.filename
|
|
3793
|
-
else:
|
|
3794
|
-
filename = "all-tabs.xlsx"
|
|
3795
|
-
else:
|
|
3796
|
-
filename = str(filepath.with_stem(self.tabname))
|
|
3797
|
-
|
|
3798
|
-
self.app.push_screen(
|
|
3799
|
-
SaveFileScreen(filename, save_all=save_all, tab_count=tab_count),
|
|
3800
|
-
callback=self.save_to_file,
|
|
3801
|
-
)
|
|
3802
|
-
|
|
3803
|
-
def save_to_file(self, result) -> None:
|
|
3804
|
-
"""Handle result from SaveFileScreen."""
|
|
3805
|
-
if result is None:
|
|
3806
|
-
return
|
|
3807
|
-
filename, save_all, overwrite_prompt = result
|
|
3808
|
-
self._save_all = save_all
|
|
3809
|
-
|
|
3810
|
-
# Check if file exists
|
|
3811
|
-
if overwrite_prompt and Path(filename).exists():
|
|
3812
|
-
self._pending_filename = filename
|
|
3813
|
-
self.app.push_screen(
|
|
3814
|
-
ConfirmScreen("File already exists. Overwrite?"),
|
|
3815
|
-
callback=self.confirm_overwrite,
|
|
3816
|
-
)
|
|
3817
|
-
else:
|
|
3818
|
-
self.save_file(filename)
|
|
3819
|
-
|
|
3820
|
-
def confirm_overwrite(self, should_overwrite: bool) -> None:
|
|
3821
|
-
"""Handle result from ConfirmScreen."""
|
|
3822
|
-
if should_overwrite:
|
|
3823
|
-
self.save_file(self._pending_filename)
|
|
3824
|
-
else:
|
|
3825
|
-
# Go back to SaveFileScreen to allow user to enter a different name
|
|
3826
|
-
self.app.push_screen(
|
|
3827
|
-
SaveFileScreen(self._pending_filename, save_all=self._save_all),
|
|
3828
|
-
callback=self.save_to_file,
|
|
3829
|
-
)
|
|
3830
|
-
|
|
3831
|
-
def save_file(self, filename: str) -> None:
|
|
3832
|
-
"""Actually save the dataframe to a file."""
|
|
3833
|
-
filepath = Path(filename)
|
|
3834
|
-
ext = filepath.suffix.lower()
|
|
3835
|
-
if ext == ".gz":
|
|
3836
|
-
ext = Path(filename).with_suffix("").suffix.lower()
|
|
3837
|
-
|
|
3838
|
-
fmt = ext.removeprefix(".")
|
|
3839
|
-
if fmt not in SUPPORTED_FORMATS:
|
|
3840
|
-
self.notify(
|
|
3841
|
-
f"Unsupported file format [$success]{fmt}[/]. Use [$accent]CSV[/] as fallback. Supported formats: {', '.join(SUPPORTED_FORMATS)}",
|
|
3842
|
-
title="Save to File",
|
|
3843
|
-
severity="warning",
|
|
3844
|
-
)
|
|
3845
|
-
fmt = "csv"
|
|
3846
|
-
|
|
3847
|
-
df = (self.df if self.df_view is None else self.df_view).select(pl.exclude(RID))
|
|
3848
|
-
try:
|
|
3849
|
-
if fmt == "csv":
|
|
3850
|
-
df.write_csv(filename)
|
|
3851
|
-
elif fmt in ("tsv", "tab"):
|
|
3852
|
-
df.write_csv(filename, separator="\t")
|
|
3853
|
-
elif fmt in ("xlsx", "xls"):
|
|
3854
|
-
self.save_excel(filename)
|
|
3855
|
-
elif fmt == "json":
|
|
3856
|
-
df.write_json(filename)
|
|
3857
|
-
elif fmt == "ndjson":
|
|
3858
|
-
df.write_ndjson(filename)
|
|
3859
|
-
elif fmt == "parquet":
|
|
3860
|
-
df.write_parquet(filename)
|
|
3861
|
-
else: # Fallback to CSV
|
|
3862
|
-
df.write_csv(filename)
|
|
3863
|
-
|
|
3864
|
-
# Update current filename
|
|
3865
|
-
self.filename = filename
|
|
3866
|
-
|
|
3867
|
-
# Reset dirty flag after save
|
|
3868
|
-
if self._save_all:
|
|
3869
|
-
tabs: dict[TabPane, DataFrameTable] = self.app.tabs
|
|
3870
|
-
for table in tabs.values():
|
|
3871
|
-
table.dirty = False
|
|
3872
|
-
else:
|
|
3873
|
-
self.dirty = False
|
|
3874
|
-
|
|
3875
|
-
if hasattr(self, "_task_after_save"):
|
|
3876
|
-
if self._task_after_save == "close_tab":
|
|
3877
|
-
self.app.do_close_tab()
|
|
3878
|
-
elif self._task_after_save == "quit_app":
|
|
3879
|
-
self.app.exit()
|
|
3880
|
-
|
|
3881
|
-
# From ConfirmScreen callback, so notify accordingly
|
|
3882
|
-
if self._save_all:
|
|
3883
|
-
self.notify(f"Saved all tabs to [$success]{filename}[/]", title="Save to File")
|
|
3884
|
-
else:
|
|
3885
|
-
self.notify(f"Saved current tab to [$success]{filename}[/]", title="Save to File")
|
|
3886
|
-
|
|
3887
|
-
except Exception as e:
|
|
3888
|
-
self.notify(f"Error saving [$error]{filename}[/]", title="Save to File", severity="error", timeout=10)
|
|
3889
|
-
self.log(f"Error saving file `{filename}`: {str(e)}")
|
|
3890
|
-
|
|
3891
|
-
def save_excel(self, filename: str) -> None:
|
|
3892
|
-
"""Save to an Excel file."""
|
|
3893
|
-
import xlsxwriter
|
|
3894
|
-
|
|
3895
|
-
if not self._save_all or len(self.app.tabs) == 1:
|
|
3896
|
-
# Single tab - save directly
|
|
3897
|
-
df = (self.df if self.df_view is None else self.df_view).select(pl.exclude(RID))
|
|
3898
|
-
df.write_excel(filename, worksheet=self.tabname)
|
|
3899
|
-
else:
|
|
3900
|
-
# Multiple tabs - use xlsxwriter to create multiple sheets
|
|
3901
|
-
with xlsxwriter.Workbook(filename) as wb:
|
|
3902
|
-
tabs: dict[TabPane, DataFrameTable] = self.app.tabs
|
|
3903
|
-
for table in tabs.values():
|
|
3904
|
-
worksheet = wb.add_worksheet(table.tabname)
|
|
3905
|
-
df = (table.df if table.df_view is None else table.df_view).select(pl.exclude(RID))
|
|
3906
|
-
df.write_excel(workbook=wb, worksheet=worksheet)
|
|
3907
|
-
|
|
3908
3773
|
# SQL Interface
|
|
3909
3774
|
def do_simple_sql(self) -> None:
|
|
3910
3775
|
"""Open the SQL interface screen."""
|
{dataframe_textual-2.4.3 → dataframe_textual-2.5.0}/src/dataframe_textual/data_frame_viewer.py
RENAMED
|
@@ -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,41 +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.
|
|
270
|
+
"""Save current tab to file, overwrite if exists."""
|
|
271
|
+
if table := self.active_table:
|
|
266
272
|
if len(self.tabs) > 1:
|
|
267
|
-
|
|
268
|
-
|
|
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
|
|
269
280
|
else:
|
|
270
281
|
filename = table.filename
|
|
271
|
-
|
|
282
|
+
|
|
283
|
+
self.save_to_file((filename, False, False))
|
|
272
284
|
|
|
273
285
|
def action_save_all_tabs_overwrite(self) -> None:
|
|
274
|
-
"""Save all
|
|
275
|
-
if table := self.
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
|
279
295
|
else:
|
|
280
|
-
filename =
|
|
296
|
+
filename = table.filename
|
|
281
297
|
|
|
282
|
-
|
|
298
|
+
self.save_to_file((filename, True, False))
|
|
283
299
|
|
|
284
300
|
def action_duplicate_tab(self) -> None:
|
|
285
301
|
"""Duplicate the currently active tab.
|
|
@@ -295,7 +311,7 @@ class DataFrameViewer(App):
|
|
|
295
311
|
Creates a copy of the current tab with the same data and filename.
|
|
296
312
|
The new tab is named with '_copy' suffix and inserted after the current tab.
|
|
297
313
|
"""
|
|
298
|
-
if not (table := self.
|
|
314
|
+
if not (table := self.active_table):
|
|
299
315
|
return
|
|
300
316
|
|
|
301
317
|
# Get current tab info
|
|
@@ -367,24 +383,6 @@ class DataFrameViewer(App):
|
|
|
367
383
|
# status = "shown" if tabs.display else "hidden"
|
|
368
384
|
# self.notify(f"Tab bar [$success]{status}[/]", title="Toggle Tab Bar")
|
|
369
385
|
|
|
370
|
-
def get_active_table(self) -> DataFrameTable | None:
|
|
371
|
-
"""Get the currently active DataFrameTable widget.
|
|
372
|
-
|
|
373
|
-
Retrieves the table from the currently active tab. Returns None if no
|
|
374
|
-
table is found or an error occurs.
|
|
375
|
-
|
|
376
|
-
Returns:
|
|
377
|
-
The active DataFrameTable widget, or None if not found.
|
|
378
|
-
"""
|
|
379
|
-
try:
|
|
380
|
-
tabbed: TabbedContent = self.query_one(TabbedContent)
|
|
381
|
-
if active_pane := tabbed.active_pane:
|
|
382
|
-
return active_pane.query_one(DataFrameTable)
|
|
383
|
-
except (NoMatches, AttributeError):
|
|
384
|
-
self.notify("No active table found", title="Locate Table", severity="error", timeout=10)
|
|
385
|
-
|
|
386
|
-
return None
|
|
387
|
-
|
|
388
386
|
def get_unique_tabname(self, tab_name: str) -> str:
|
|
389
387
|
"""Generate a unique tab name based on the given base name.
|
|
390
388
|
|
|
@@ -495,17 +493,14 @@ class DataFrameViewer(App):
|
|
|
495
493
|
can be closed, the application exits instead.
|
|
496
494
|
"""
|
|
497
495
|
try:
|
|
498
|
-
if not (
|
|
499
|
-
return
|
|
500
|
-
|
|
501
|
-
if not (active_table := self.tabs.get(active_pane)):
|
|
496
|
+
if not (table := self.active_table):
|
|
502
497
|
return
|
|
503
498
|
|
|
504
499
|
def _on_save_confirm(result: bool) -> None:
|
|
505
500
|
"""Handle the "save before closing?" confirmation."""
|
|
506
501
|
if result:
|
|
507
502
|
# User wants to save - close after save dialog opens
|
|
508
|
-
|
|
503
|
+
self.do_save_to_file(all_tabs=False, task_after_save="close_tab")
|
|
509
504
|
elif result is None:
|
|
510
505
|
# User cancelled - do nothing
|
|
511
506
|
return
|
|
@@ -513,7 +508,7 @@ class DataFrameViewer(App):
|
|
|
513
508
|
# User wants to discard - close immediately
|
|
514
509
|
self.close_tab()
|
|
515
510
|
|
|
516
|
-
if
|
|
511
|
+
if table.dirty:
|
|
517
512
|
self.push_screen(
|
|
518
513
|
ConfirmScreen(
|
|
519
514
|
"Close Tab",
|
|
@@ -560,7 +555,7 @@ class DataFrameViewer(App):
|
|
|
560
555
|
|
|
561
556
|
def _save_and_quit(result: bool) -> None:
|
|
562
557
|
if result:
|
|
563
|
-
self.
|
|
558
|
+
self.do_save_to_file(all_tabs=True, task_after_save="quit_app")
|
|
564
559
|
elif result is None:
|
|
565
560
|
# User cancelled - do nothing
|
|
566
561
|
return
|
|
@@ -568,15 +563,17 @@ class DataFrameViewer(App):
|
|
|
568
563
|
# User wants to discard - quit immediately
|
|
569
564
|
self.exit()
|
|
570
565
|
|
|
566
|
+
tab_count = len(self.tabs)
|
|
571
567
|
tab_list = "\n".join(f" - [$warning]{name}[/]" for name in dirty_tabnames)
|
|
572
568
|
label = (
|
|
573
569
|
f"The following tabs have unsaved changes:\n\n{tab_list}\n\nSave all changes?"
|
|
574
570
|
if len(dirty_tabnames) > 1
|
|
575
571
|
else f"The tab [$warning]{dirty_tabnames[0]}[/] has unsaved changes.\n\nSave changes?"
|
|
576
572
|
)
|
|
573
|
+
|
|
577
574
|
self.push_screen(
|
|
578
575
|
ConfirmScreen(
|
|
579
|
-
"Close
|
|
576
|
+
f"Close {tab_count} Tabs" if tab_count > 1 else "Close Tab",
|
|
580
577
|
label=label,
|
|
581
578
|
yes="Save",
|
|
582
579
|
maybe="Discard",
|
|
@@ -631,3 +628,141 @@ class DataFrameViewer(App):
|
|
|
631
628
|
break
|
|
632
629
|
|
|
633
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)
|
|
@@ -298,7 +298,7 @@ class SaveFileScreen(YesNoScreen):
|
|
|
298
298
|
title="Save to File",
|
|
299
299
|
label="Filename",
|
|
300
300
|
input=filename,
|
|
301
|
-
yes=f"Save {tab_count}
|
|
301
|
+
yes=f"Save {tab_count} Tabs" if self.save_all else "Save Current Tab" if tab_count > 1 else "Save",
|
|
302
302
|
no="Cancel",
|
|
303
303
|
on_yes_callback=self.handle_save,
|
|
304
304
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dataframe_textual-2.4.3 → dataframe_textual-2.5.0}/src/dataframe_textual/data_frame_help_panel.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|