dataframe-textual 2.4.3__tar.gz → 2.6.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.6.0}/PKG-INFO +6 -6
- {dataframe_textual-2.4.3 → dataframe_textual-2.6.0}/README.md +5 -5
- {dataframe_textual-2.4.3 → dataframe_textual-2.6.0}/pyproject.toml +1 -1
- {dataframe_textual-2.4.3 → dataframe_textual-2.6.0}/src/dataframe_textual/data_frame_table.py +98 -189
- {dataframe_textual-2.4.3 → dataframe_textual-2.6.0}/src/dataframe_textual/data_frame_viewer.py +196 -61
- {dataframe_textual-2.4.3 → dataframe_textual-2.6.0}/src/dataframe_textual/yes_no_screen.py +1 -1
- {dataframe_textual-2.4.3 → dataframe_textual-2.6.0}/uv.lock +1 -1
- {dataframe_textual-2.4.3 → dataframe_textual-2.6.0}/.gitignore +0 -0
- {dataframe_textual-2.4.3 → dataframe_textual-2.6.0}/1811.csv.gz +0 -0
- {dataframe_textual-2.4.3 → dataframe_textual-2.6.0}/LICENSE +0 -0
- {dataframe_textual-2.4.3 → dataframe_textual-2.6.0}/large_malformed.tsv.gz +0 -0
- {dataframe_textual-2.4.3 → dataframe_textual-2.6.0}/main.py +0 -0
- {dataframe_textual-2.4.3 → dataframe_textual-2.6.0}/src/dataframe_textual/__init__.py +0 -0
- {dataframe_textual-2.4.3 → dataframe_textual-2.6.0}/src/dataframe_textual/__main__.py +0 -0
- {dataframe_textual-2.4.3 → dataframe_textual-2.6.0}/src/dataframe_textual/common.py +0 -0
- {dataframe_textual-2.4.3 → dataframe_textual-2.6.0}/src/dataframe_textual/data_frame_help_panel.py +0 -0
- {dataframe_textual-2.4.3 → dataframe_textual-2.6.0}/src/dataframe_textual/sql_screen.py +0 -0
- {dataframe_textual-2.4.3 → dataframe_textual-2.6.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.6.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 |
|
|
@@ -314,7 +314,8 @@ zcat compressed_data.csv.gz | dv -f csv
|
|
|
314
314
|
| Key | Action |
|
|
315
315
|
|-----|--------|
|
|
316
316
|
| `Double-click` | Edit cell or rename column header |
|
|
317
|
-
| `
|
|
317
|
+
| `Delete` | Clear current cell (set to NULL) |
|
|
318
|
+
| `Shift+Delete` | Clear current column (set matching cells to NULL) |
|
|
318
319
|
| `e` | Edit current cell (respects data type) |
|
|
319
320
|
| `E` | Edit entire column with value/expression |
|
|
320
321
|
| `a` | Add empty column after current |
|
|
@@ -391,14 +392,13 @@ zcat compressed_data.csv.gz | dv -f csv
|
|
|
391
392
|
| `!` | Cast current column to boolean |
|
|
392
393
|
| `$` | Cast current column to string |
|
|
393
394
|
|
|
394
|
-
#### Copy
|
|
395
|
+
#### Copy
|
|
395
396
|
|
|
396
397
|
| Key | Action |
|
|
397
398
|
|-----|--------|
|
|
398
399
|
| `c` | Copy current cell to clipboard |
|
|
399
400
|
| `Ctrl+C` | Copy column to clipboard |
|
|
400
401
|
| `Ctrl+R` | Copy row to clipboard (tab-separated) |
|
|
401
|
-
| `Ctrl+S` | Save to file |
|
|
402
402
|
|
|
403
403
|
## Features in Detail
|
|
404
404
|
|
|
@@ -820,7 +820,7 @@ Manage multiple files and dataframes simultaneously with tabs.
|
|
|
820
820
|
- **`Double-click`** - Rename the tab
|
|
821
821
|
- **`Ctrl+D`** - Duplicate current tab (creates a copy with same data and state)
|
|
822
822
|
- **`Ctrl+T`** - Save current tab to file
|
|
823
|
-
- **`Ctrl+
|
|
823
|
+
- **`Ctrl+S`** - Save all tabs to file
|
|
824
824
|
- **`w`** - Save current tab to file (overwrite without prompt)
|
|
825
825
|
- **`W`** - Save all tabs to file (overwrite without prompt)
|
|
826
826
|
- **`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 |
|
|
@@ -275,7 +275,8 @@ zcat compressed_data.csv.gz | dv -f csv
|
|
|
275
275
|
| Key | Action |
|
|
276
276
|
|-----|--------|
|
|
277
277
|
| `Double-click` | Edit cell or rename column header |
|
|
278
|
-
| `
|
|
278
|
+
| `Delete` | Clear current cell (set to NULL) |
|
|
279
|
+
| `Shift+Delete` | Clear current column (set matching cells to NULL) |
|
|
279
280
|
| `e` | Edit current cell (respects data type) |
|
|
280
281
|
| `E` | Edit entire column with value/expression |
|
|
281
282
|
| `a` | Add empty column after current |
|
|
@@ -352,14 +353,13 @@ zcat compressed_data.csv.gz | dv -f csv
|
|
|
352
353
|
| `!` | Cast current column to boolean |
|
|
353
354
|
| `$` | Cast current column to string |
|
|
354
355
|
|
|
355
|
-
#### Copy
|
|
356
|
+
#### Copy
|
|
356
357
|
|
|
357
358
|
| Key | Action |
|
|
358
359
|
|-----|--------|
|
|
359
360
|
| `c` | Copy current cell to clipboard |
|
|
360
361
|
| `Ctrl+C` | Copy column to clipboard |
|
|
361
362
|
| `Ctrl+R` | Copy row to clipboard (tab-separated) |
|
|
362
|
-
| `Ctrl+S` | Save to file |
|
|
363
363
|
|
|
364
364
|
## Features in Detail
|
|
365
365
|
|
|
@@ -781,7 +781,7 @@ Manage multiple files and dataframes simultaneously with tabs.
|
|
|
781
781
|
- **`Double-click`** - Rename the tab
|
|
782
782
|
- **`Ctrl+D`** - Duplicate current tab (creates a copy with same data and state)
|
|
783
783
|
- **`Ctrl+T`** - Save current tab to file
|
|
784
|
-
- **`Ctrl+
|
|
784
|
+
- **`Ctrl+S`** - Save all tabs to file
|
|
785
785
|
- **`w`** - Save current tab to file (overwrite without prompt)
|
|
786
786
|
- **`W`** - Save all tabs to file (overwrite without prompt)
|
|
787
787
|
- **`q`** - Close current tab (closes tab, prompts to save if unsaved changes)
|
{dataframe_textual-2.4.3 → dataframe_textual-2.6.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
|
|
|
@@ -209,6 +207,7 @@ class DataFrameTable(DataTable):
|
|
|
209
207
|
- **X** - ❌ Delete row and those below
|
|
210
208
|
- **Ctrl+X** - ❌ Delete row and those above
|
|
211
209
|
- **delete** - ❌ Clear current cell (set to NULL)
|
|
210
|
+
- **Shift+Delete** - ❌ Clear current column (set matching cells to NULL)
|
|
212
211
|
- **-** - ❌ Delete current column
|
|
213
212
|
- **d** - 📋 Duplicate current column
|
|
214
213
|
- **D** - 📋 Duplicate current row
|
|
@@ -223,11 +222,10 @@ class DataFrameTable(DataTable):
|
|
|
223
222
|
- **!** - ✅ Cast column to boolean
|
|
224
223
|
- **$** - 📝 Cast column to string
|
|
225
224
|
|
|
226
|
-
## 💾 Copy
|
|
225
|
+
## 💾 Copy
|
|
227
226
|
- **c** - 📋 Copy cell to clipboard
|
|
228
227
|
- **Ctrl+c** - 📊 Copy column to clipboard
|
|
229
228
|
- **Ctrl+r** - 📝 Copy row to clipboard (tab-separated)
|
|
230
|
-
- **Ctrl+s** - 💾 Save current tab to file
|
|
231
229
|
""").strip()
|
|
232
230
|
|
|
233
231
|
# fmt: off
|
|
@@ -255,8 +253,6 @@ class DataFrameTable(DataTable):
|
|
|
255
253
|
("c", "copy_cell", "Copy cell to clipboard"),
|
|
256
254
|
("ctrl+c", "copy_column", "Copy column to clipboard"),
|
|
257
255
|
("ctrl+r", "copy_row", "Copy row to clipboard"),
|
|
258
|
-
# Save
|
|
259
|
-
("ctrl+s", "save_to_file", "Save to file"),
|
|
260
256
|
# Metadata, Detail, Frequency, and Statistics
|
|
261
257
|
("m", "metadata_shape", "Show metadata for row count and column count"),
|
|
262
258
|
("M", "metadata_column", "Show metadata for column"),
|
|
@@ -290,6 +286,7 @@ class DataFrameTable(DataTable):
|
|
|
290
286
|
("R", "replace_global", "Replace global"), # `Shift+R`
|
|
291
287
|
# Delete
|
|
292
288
|
("delete", "clear_cell", "Clear cell"),
|
|
289
|
+
("shift+delete", "clear_column", "Clear cells in current column that match cursor value"), # `Shift+Delete`
|
|
293
290
|
("minus", "delete_column", "Delete column"), # `-`
|
|
294
291
|
("x", "delete_row", "Delete row"),
|
|
295
292
|
("X", "delete_row_and_below", "Delete row and those below"),
|
|
@@ -744,10 +741,6 @@ class DataFrameTable(DataTable):
|
|
|
744
741
|
"""Sort by current column in descending order."""
|
|
745
742
|
self.do_sort_by_column(descending=True)
|
|
746
743
|
|
|
747
|
-
def action_save_to_file(self) -> None:
|
|
748
|
-
"""Save the current dataframe to a file."""
|
|
749
|
-
self.do_save_to_file()
|
|
750
|
-
|
|
751
744
|
def action_show_frequency(self) -> None:
|
|
752
745
|
"""Show frequency distribution for the current column."""
|
|
753
746
|
self.do_show_frequency()
|
|
@@ -804,6 +797,10 @@ class DataFrameTable(DataTable):
|
|
|
804
797
|
"""Clear the current cell (set to None)."""
|
|
805
798
|
self.do_clear_cell()
|
|
806
799
|
|
|
800
|
+
def action_clear_column(self) -> None:
|
|
801
|
+
"""Clear cells in the current column that match the cursor value."""
|
|
802
|
+
self.do_clear_column()
|
|
803
|
+
|
|
807
804
|
def action_select_row(self) -> None:
|
|
808
805
|
"""Select rows with cursor value in the current column."""
|
|
809
806
|
self.do_select_row()
|
|
@@ -2052,25 +2049,10 @@ class DataFrameTable(DataTable):
|
|
|
2052
2049
|
# Also update the view if applicable
|
|
2053
2050
|
# Update the value of col_name in df_view using the value of col_name from df based on RID mapping between them
|
|
2054
2051
|
if self.df_view is not None:
|
|
2055
|
-
# Get updated column from df
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
RID, pl.col(col_name).alias(col_updated), pl.lit(True).alias(col_exists)
|
|
2060
|
-
)
|
|
2061
|
-
# Join and use when/then/otherwise to handle all updates including NULLs
|
|
2062
|
-
self.df_view = (
|
|
2063
|
-
self.df_view.lazy()
|
|
2064
|
-
.join(lf_updated, on=RID, how="left")
|
|
2065
|
-
.with_columns(
|
|
2066
|
-
pl.when(pl.col(col_exists))
|
|
2067
|
-
.then(pl.col(col_updated))
|
|
2068
|
-
.otherwise(pl.col(col_name))
|
|
2069
|
-
.alias(col_name)
|
|
2070
|
-
)
|
|
2071
|
-
.drop(col_updated, col_exists)
|
|
2072
|
-
.collect()
|
|
2073
|
-
)
|
|
2052
|
+
# Get updated column from df
|
|
2053
|
+
lf_updated = self.df.lazy().select(RID, pl.col(col_name))
|
|
2054
|
+
# Update df_view by joining on RID
|
|
2055
|
+
self.df_view = self.df_view.lazy().update(lf_updated, on=RID, include_nulls=True).collect()
|
|
2074
2056
|
except Exception as e:
|
|
2075
2057
|
self.notify(
|
|
2076
2058
|
f"Error applying expression: [$error]{term}[/] to column [$accent]{col_name}[/]",
|
|
@@ -2156,18 +2138,26 @@ class DataFrameTable(DataTable):
|
|
|
2156
2138
|
|
|
2157
2139
|
# Update the cell to None in the dataframe
|
|
2158
2140
|
try:
|
|
2159
|
-
self.df =
|
|
2160
|
-
|
|
2161
|
-
.
|
|
2162
|
-
|
|
2163
|
-
|
|
2141
|
+
self.df = (
|
|
2142
|
+
self.df.lazy()
|
|
2143
|
+
.with_columns(
|
|
2144
|
+
pl.when(pl.arange(0, len(self.df)) == ridx)
|
|
2145
|
+
.then(pl.lit(None))
|
|
2146
|
+
.otherwise(pl.col(col_name))
|
|
2147
|
+
.alias(col_name)
|
|
2148
|
+
)
|
|
2149
|
+
.collect()
|
|
2164
2150
|
)
|
|
2165
2151
|
|
|
2166
2152
|
# Also update the view if applicable
|
|
2167
2153
|
if self.df_view is not None:
|
|
2168
2154
|
ridx_view = self.df.item(ridx, self.df.columns.index(RID))
|
|
2169
|
-
self.df_view =
|
|
2170
|
-
|
|
2155
|
+
self.df_view = (
|
|
2156
|
+
self.df_view.lazy()
|
|
2157
|
+
.with_columns(
|
|
2158
|
+
pl.when(pl.col(RID) == ridx_view).then(pl.lit(None)).otherwise(pl.col(col_name)).alias(col_name)
|
|
2159
|
+
)
|
|
2160
|
+
.collect()
|
|
2171
2161
|
)
|
|
2172
2162
|
|
|
2173
2163
|
# Update the display
|
|
@@ -2186,7 +2176,44 @@ class DataFrameTable(DataTable):
|
|
|
2186
2176
|
timeout=10,
|
|
2187
2177
|
)
|
|
2188
2178
|
self.log(f"Error clearing cell ({ridx}, {col_name}): {str(e)}")
|
|
2189
|
-
|
|
2179
|
+
|
|
2180
|
+
def do_clear_column(self) -> None:
|
|
2181
|
+
"""Clear the current column by setting all its values to None."""
|
|
2182
|
+
col_idx = self.cursor_column
|
|
2183
|
+
col_name = self.cursor_col_name
|
|
2184
|
+
value = self.cursor_value
|
|
2185
|
+
|
|
2186
|
+
# Add to history
|
|
2187
|
+
self.add_history(f"Cleared column [$success]{col_name}[/]", dirty=True)
|
|
2188
|
+
|
|
2189
|
+
try:
|
|
2190
|
+
# Update the entire column to None in the dataframe
|
|
2191
|
+
self.df = (
|
|
2192
|
+
self.df.lazy()
|
|
2193
|
+
.with_columns(
|
|
2194
|
+
pl.when(pl.col(col_name) == value).then(pl.lit(None)).otherwise(pl.col(col_name)).alias(col_name)
|
|
2195
|
+
)
|
|
2196
|
+
.collect()
|
|
2197
|
+
)
|
|
2198
|
+
|
|
2199
|
+
# Also update the view if applicable
|
|
2200
|
+
if self.df_view is not None:
|
|
2201
|
+
self.df_view = self.df_view.with_columns(
|
|
2202
|
+
pl.when(pl.col(col_name) == value).then(pl.lit(None)).otherwise(pl.col(col_name)).alias(col_name)
|
|
2203
|
+
)
|
|
2204
|
+
|
|
2205
|
+
# Recreate table for display
|
|
2206
|
+
self.setup_table()
|
|
2207
|
+
|
|
2208
|
+
# Move cursor to the cleared column
|
|
2209
|
+
self.move_cursor(column=col_idx)
|
|
2210
|
+
|
|
2211
|
+
# self.notify(f"Cleared column [$success]{col_name}[/]", title="Clear Column")
|
|
2212
|
+
except Exception as e:
|
|
2213
|
+
self.notify(
|
|
2214
|
+
f"Error clearing column [$error]{col_name}[/]", title="Clear Column", severity="error", timeout=10
|
|
2215
|
+
)
|
|
2216
|
+
self.log(f"Error clearing column `{col_name}`: {str(e)}")
|
|
2190
2217
|
|
|
2191
2218
|
def do_add_column(self, col_name: str = None) -> None:
|
|
2192
2219
|
"""Add acolumn after the current column."""
|
|
@@ -3394,18 +3421,23 @@ class DataFrameTable(DataTable):
|
|
|
3394
3421
|
# Only applicable to string columns for substring matches
|
|
3395
3422
|
if dtype == pl.String and not state.match_whole:
|
|
3396
3423
|
term_find = f"(?i){state.term_find}" if state.match_nocase else state.term_find
|
|
3424
|
+
new_value = (
|
|
3425
|
+
pl.lit(None)
|
|
3426
|
+
if state.term_replace == NULL
|
|
3427
|
+
else pl.col(col_name).str.replace_all(term_find, state.term_replace)
|
|
3428
|
+
)
|
|
3397
3429
|
self.df = self.df.with_columns(
|
|
3398
|
-
pl.when(mask)
|
|
3399
|
-
.then(pl.col(col_name).str.replace_all(term_find, state.term_replace))
|
|
3400
|
-
.otherwise(pl.col(col_name))
|
|
3401
|
-
.alias(col_name)
|
|
3430
|
+
pl.when(mask).then(new_value).otherwise(pl.col(col_name)).alias(col_name)
|
|
3402
3431
|
)
|
|
3403
3432
|
else:
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3433
|
+
if state.term_replace == NULL:
|
|
3434
|
+
value = None
|
|
3435
|
+
else:
|
|
3436
|
+
# Try to convert replacement value to column dtype
|
|
3437
|
+
try:
|
|
3438
|
+
value = DtypeConfig(dtype).convert(state.term_replace)
|
|
3439
|
+
except Exception:
|
|
3440
|
+
value = state.term_replace
|
|
3409
3441
|
|
|
3410
3442
|
self.df = self.df.with_columns(
|
|
3411
3443
|
pl.when(mask).then(pl.lit(value)).otherwise(pl.col(col_name)).alias(col_name)
|
|
@@ -3413,15 +3445,8 @@ class DataFrameTable(DataTable):
|
|
|
3413
3445
|
|
|
3414
3446
|
# Also update the view if applicable
|
|
3415
3447
|
if self.df_view is not None:
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
self.df_view = (
|
|
3419
|
-
self.df_view.lazy()
|
|
3420
|
-
.join(lf_updated, on=RID, how="left")
|
|
3421
|
-
.with_columns(pl.coalesce(pl.col(col_updated), pl.col(col_name)).alias(col_name))
|
|
3422
|
-
.drop(col_updated)
|
|
3423
|
-
.collect()
|
|
3424
|
-
)
|
|
3448
|
+
lf_updated = self.df.lazy().filter(mask).select(pl.col(RID), pl.col(col_name))
|
|
3449
|
+
self.df_view = self.df_view.lazy().update(lf_updated, on=RID, include_nulls=True).collect()
|
|
3425
3450
|
|
|
3426
3451
|
state.replaced_occurrence += len(ridxs)
|
|
3427
3452
|
|
|
@@ -3500,9 +3525,14 @@ class DataFrameTable(DataTable):
|
|
|
3500
3525
|
# Only applicable to string columns for substring matches
|
|
3501
3526
|
if dtype == pl.String and not state.match_whole:
|
|
3502
3527
|
term_find = f"(?i){state.term_find}" if state.match_nocase else state.term_find
|
|
3528
|
+
new_value = (
|
|
3529
|
+
pl.lit(None)
|
|
3530
|
+
if state.term_replace == NULL
|
|
3531
|
+
else pl.col(col_name).str.replace_all(term_find, state.term_replace)
|
|
3532
|
+
)
|
|
3503
3533
|
self.df = self.df.with_columns(
|
|
3504
3534
|
pl.when(pl.arange(0, len(self.df)) == ridx)
|
|
3505
|
-
.then(
|
|
3535
|
+
.then(new_value)
|
|
3506
3536
|
.otherwise(pl.col(col_name))
|
|
3507
3537
|
.alias(col_name)
|
|
3508
3538
|
)
|
|
@@ -3516,11 +3546,14 @@ class DataFrameTable(DataTable):
|
|
|
3516
3546
|
.alias(col_name)
|
|
3517
3547
|
)
|
|
3518
3548
|
else:
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3549
|
+
if state.term_replace == NULL:
|
|
3550
|
+
value = None
|
|
3551
|
+
else:
|
|
3552
|
+
# try to convert replacement value to column dtype
|
|
3553
|
+
try:
|
|
3554
|
+
value = DtypeConfig(dtype).convert(state.term_replace)
|
|
3555
|
+
except Exception:
|
|
3556
|
+
value = state.term_replace
|
|
3524
3557
|
|
|
3525
3558
|
self.df = self.df.with_columns(
|
|
3526
3559
|
pl.when(pl.arange(0, len(self.df)) == ridx)
|
|
@@ -3548,6 +3581,8 @@ class DataFrameTable(DataTable):
|
|
|
3548
3581
|
if not state.done:
|
|
3549
3582
|
# Get the new value of the current cell after replacement
|
|
3550
3583
|
new_cell_value = self.df.item(ridx, cidx)
|
|
3584
|
+
if new_cell_value is None:
|
|
3585
|
+
new_cell_value = NULL_DISPLAY
|
|
3551
3586
|
row_key = str(ridx)
|
|
3552
3587
|
col_key = col_name
|
|
3553
3588
|
self.update_cell(
|
|
@@ -3755,7 +3790,7 @@ class DataFrameTable(DataTable):
|
|
|
3755
3790
|
|
|
3756
3791
|
self.notify(f"{message}. Now showing [$success]{len(self.df)}[/] rows.", title="Filter Rows")
|
|
3757
3792
|
|
|
3758
|
-
# Copy
|
|
3793
|
+
# Copy
|
|
3759
3794
|
def do_copy_to_clipboard(self, content: str, message: str) -> None:
|
|
3760
3795
|
"""Copy content to clipboard using pbcopy (macOS) or xclip (Linux).
|
|
3761
3796
|
|
|
@@ -3779,132 +3814,6 @@ class DataFrameTable(DataTable):
|
|
|
3779
3814
|
except FileNotFoundError:
|
|
3780
3815
|
self.notify("Error copying to clipboard", title="Copy to Clipboard", severity="error", timeout=10)
|
|
3781
3816
|
|
|
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
3817
|
# SQL Interface
|
|
3909
3818
|
def do_simple_sql(self) -> None:
|
|
3910
3819
|
"""Open the SQL interface screen."""
|
{dataframe_textual-2.4.3 → dataframe_textual-2.6.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.6.0}/src/dataframe_textual/data_frame_help_panel.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|