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.
@@ -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+A** - 💾 Save all tabs to file
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+a", "save_all_tabs", "Save All Tabs"),
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.get_active_table().focus()
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.get_active_table():
220
+ if table := self.active_table:
205
221
  table.focus()
206
- else:
207
- return
208
222
 
209
- if table.loaded_rows == 0:
210
- table.setup_table()
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
- """Save the currently active tab to file.
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
- """Save all open tabs to their respective files.
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 the currently active tab to file, overwriting if it exists."""
265
- if table := self.get_active_table():
266
- filepath = Path(table.filename)
267
- filename = filepath.with_stem(table.tabname)
268
- table.save_to_file((filename, False, False))
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 open tabs to their respective files, overwriting if they exist."""
272
- if table := self.get_active_table():
273
- filepath = Path(table.filename)
274
- if filepath.suffix.lower() in [".xlsx", ".xls"]:
275
- filename = table.filename
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 = "all-tabs.xlsx"
296
+ filename = table.filename
278
297
 
279
- table.save_to_file((filename, True, False))
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.get_active_table()):
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 (active_pane := self.tabbed.active_pane):
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
- active_table.do_save_to_file(task_after_save="close_tab")
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 active_table.dirty:
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.get_active_table()._save_to_file(task_after_save="quit_app")
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 All Tabs" if len(self.tabs) > 1 else "Close Tab",
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.4.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,excel,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 ...]
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,excel,ndjson,psv,parquet,tsv}
181
- Specify the format of the input files (csv, excel, tsv etc.)
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 [COMMENT_PREFIX]
188
- Comment lines are skipped when reading CSV/TSV
189
- -q, --quote-char [QUOTE_CHAR]
190
- Quote character for reading CSV/TSV
191
- -l, --skip-lines SKIP_LINES
192
- Skip lines when reading CSV/TSV
193
- -a, --skip-rows-after-header SKIP_ROWS_AFTER_HEADER
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+A` | Save all tabs to a Excel file |
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
- #### Data Editing
313
+ #### Editing
313
314
 
314
315
  | Key | Action |
315
316
  |-----|--------|
316
317
  | `Double-click` | Edit cell or rename column header |
317
- | `delete` | Clear current cell (set to NULL) |
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 & Save
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
- | `Ctrl+S` | Save to file |
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. Data Editing
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+A`** - Save all tabs in a single Excel file
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,,