dataframe-textual 1.4.0__py3-none-any.whl → 1.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,21 +1,21 @@
1
1
  """DataFrame Viewer application and utilities."""
2
2
 
3
3
  import os
4
- from functools import partial
5
4
  from pathlib import Path
6
5
  from textwrap import dedent
7
6
 
8
7
  import polars as pl
9
8
  from textual.app import App, ComposeResult
10
9
  from textual.css.query import NoMatches
10
+ from textual.events import Click
11
11
  from textual.theme import BUILTIN_THEMES
12
12
  from textual.widgets import TabbedContent, TabPane
13
- from textual.widgets.tabbed_content import ContentTabs
13
+ from textual.widgets.tabbed_content import ContentTab, ContentTabs
14
14
 
15
- from .common import get_next_item, load_file
15
+ from .common import 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 OpenFileScreen, SaveFileScreen
18
+ from .yes_no_screen import ConfirmScreen, OpenFileScreen, RenameTabScreen
19
19
 
20
20
 
21
21
  class DataFrameViewer(App):
@@ -25,13 +25,18 @@ class DataFrameViewer(App):
25
25
  # 📊 DataFrame Viewer - App Controls
26
26
 
27
27
  ## 🎯 File & Tab Management
28
- - **Ctrl+O** - 📁 Add a new tab
29
- - **Ctrl+A** - 💾 Save all tabs
30
- - **Ctrl+W** - ❌ Close current tab
31
- - **>** or **b** - ▶️ Next tab
28
+ - **>** - ▶️ Next tab
32
29
  - **<** - ◀️ Previous tab
30
+ - **b** - 🔄 Cycle through tabs
33
31
  - **B** - 👁️ Toggle tab bar visibility
34
- - **q** - 🚪 Quit application
32
+ - **q** - Close current tab (prompts to save unsaved changes)
33
+ - **Q** - ❌ Close all tabs (prompts to save unsaved changes)
34
+ - **Ctrl+Q** - 🚪 Force to quit app (discards unsaved changes)
35
+ - **Ctrl+T** - 💾 Save current tab to file
36
+ - **Ctrl+A** - 💾 Save all tabs to file
37
+ - **Ctrl+D** - 📋 Duplicate current tab
38
+ - **Ctrl+O** - 📁 Open a file
39
+ - **Double-click tab** - ✏️ Rename current tab
35
40
 
36
41
  ## 🎨 View & Settings
37
42
  - **F1** - ❓ Toggle this help panel
@@ -39,25 +44,27 @@ class DataFrameViewer(App):
39
44
 
40
45
  ## ⭐ Features
41
46
  - **Multi-file support** - 📂 Open multiple CSV/Excel files as tabs
42
- - **Excel sheets** - 📊 Excel files auto-expand sheets into tabs
43
47
  - **Lazy loading** - ⚡ Large files load on demand
44
48
  - **Sticky tabs** - 📌 Tab bar stays visible when scrolling
49
+ - **Unsaved changes** - 🔴 Tabs with unsaved changes have a bright bottom border
45
50
  - **Rich formatting** - 🎨 Color-coded data types
46
51
  - **Search & filter** - 🔍 Find and filter data quickly
47
- - **Sort & reorder** - ⬆️ Multi-column sort, drag rows/columns
48
- - **Undo/Redo** - 🔄 Full history of operations
52
+ - **Sort & reorder** - ⬆️ Multi-column sort, reorder rows/columns
53
+ - **Undo/Redo/Reset** - 🔄 Full history of operations
49
54
  - **Freeze rows/cols** - 🔒 Pin header rows and columns
50
55
  """).strip()
51
56
 
52
57
  BINDINGS = [
53
- ("q", "quit", "Quit"),
54
- ("f1", "toggle_help_panel", "Help"),
58
+ ("q", "close_tab", "Close current tab"),
59
+ ("Q", "close_all_tabs", "Close all tabs and quit app"),
55
60
  ("B", "toggle_tab_bar", "Toggle Tab Bar"),
56
- ("ctrl+o", "add_tab", "Add Tab"),
61
+ ("f1", "toggle_help_panel", "Help"),
62
+ ("ctrl+o", "open_file", "Open File"),
63
+ ("ctrl+t", "save_current_tab", "Save Current Tab"),
57
64
  ("ctrl+a", "save_all_tabs", "Save All Tabs"),
58
- ("ctrl+w", "close_tab", "Close Tab"),
59
- ("greater_than_sign,b", "next_tab(1)", "Next Tab"),
60
- ("less_than_sign", "next_tab(-1)", "Prev Tab"),
65
+ ("ctrl+d", "duplicate_tab", "Duplicate Tab"),
66
+ ("greater_than_sign,b", "next_tab(1)", "Next Tab"), # '>' and 'b'
67
+ ("less_than_sign", "next_tab(-1)", "Prev Tab"), # '<'
61
68
  ]
62
69
 
63
70
  CSS = """
@@ -71,9 +78,16 @@ class DataFrameViewer(App):
71
78
  ContentTab.-active {
72
79
  background: $block-cursor-background; /* Same as underline */
73
80
  }
81
+ ContentTab.dirty {
82
+ background: $warning-darken-3;
83
+ }
84
+
85
+ .underline--bar {
86
+ color: red;
87
+ }
74
88
  """
75
89
 
76
- def __init__(self, *sources: str) -> None:
90
+ def __init__(self, *sources: Source) -> None:
77
91
  """Initialize the DataFrame Viewer application.
78
92
 
79
93
  Loads data from provided sources and prepares the tabbed interface.
@@ -81,9 +95,6 @@ class DataFrameViewer(App):
81
95
  Args:
82
96
  sources: sources to load dataframes from, each as a tuple of
83
97
  (DataFrame, filename, tabname).
84
-
85
- Returns:
86
- None
87
98
  """
88
99
  super().__init__()
89
100
  self.sources = sources
@@ -103,8 +114,9 @@ class DataFrameViewer(App):
103
114
  self.tabbed = TabbedContent(id="main_tabs")
104
115
  with self.tabbed:
105
116
  seen_names = set()
106
- for idx, (df, filename, tabname) in enumerate(self.sources, start=1):
107
- tab_id = f"tab_{idx}"
117
+ for idx, source in enumerate(self.sources, start=1):
118
+ df, filename, tabname = source.frame, source.filename, source.tabname
119
+ tab_id = f"tab-{idx}"
108
120
 
109
121
  if not tabname:
110
122
  tabname = Path(filename).stem or tab_id
@@ -117,8 +129,8 @@ class DataFrameViewer(App):
117
129
  seen_names.add(tabname)
118
130
 
119
131
  try:
120
- table = DataFrameTable(df, filename, name=tabname, id=tab_id, zebra_stripes=True)
121
- tab = TabPane(tabname, table, name=tabname, id=tab_id)
132
+ table = DataFrameTable(df, filename, tabname=tabname, id=tab_id, zebra_stripes=True)
133
+ tab = TabPane(tabname, table, id=tab_id)
122
134
  self.tabs[tab] = table
123
135
  yield tab
124
136
  except Exception as e:
@@ -133,13 +145,15 @@ class DataFrameViewer(App):
133
145
 
134
146
  Initializes the app by hiding the tab bar for single-file mode and focusing
135
147
  the active table widget.
136
-
137
- Returns:
138
- None
139
148
  """
140
149
  if len(self.tabs) == 1:
141
150
  self.query_one(ContentTabs).display = False
142
- self._get_active_table().focus()
151
+ self.get_active_table().focus()
152
+
153
+ def on_ready(self) -> None:
154
+ """Called when the app is ready."""
155
+ # self.log(self.tree)
156
+ pass
143
157
 
144
158
  def on_key(self, event) -> None:
145
159
  """Handle key press events at the application level.
@@ -148,14 +162,32 @@ class DataFrameViewer(App):
148
162
 
149
163
  Args:
150
164
  event: The key event object containing key information.
151
-
152
- Returns:
153
- None
154
165
  """
155
166
  if event.key == "k":
156
167
  self.theme = get_next_item(list(BUILTIN_THEMES.keys()), self.theme)
157
168
  self.notify(f"Switched to theme: [$success]{self.theme}[/]", title="Theme")
158
169
 
170
+ def on_click(self, event: Click) -> None:
171
+ """Handle mouse click events on tabs.
172
+
173
+ Detects double-clicks on tab headers and opens the rename screen.
174
+
175
+ Args:
176
+ event: The click event containing position information.
177
+ """
178
+ # Check if this is a double-click (chain > 1) on a tab header
179
+ if event.chain > 1:
180
+ try:
181
+ # Get the widget that was clicked
182
+ content_tab = event.widget
183
+
184
+ # Check if it's a ContentTab (tab header)
185
+ if isinstance(content_tab, ContentTab):
186
+ self.do_rename_tab(content_tab)
187
+ except Exception as e:
188
+ self.log(f"Error handling tab rename click: {str(e)}")
189
+ pass
190
+
159
191
  def on_tabbed_content_tab_activated(self, event: TabbedContent.TabActivated) -> None:
160
192
  """Handle tab activation events.
161
193
 
@@ -164,26 +196,20 @@ class DataFrameViewer(App):
164
196
 
165
197
  Args:
166
198
  event: The tab activated event containing the activated tab pane.
167
-
168
- Returns:
169
- None
170
199
  """
171
200
  # Focus the table in the newly activated tab
172
- if table := self._get_active_table():
201
+ if table := self.get_active_table():
173
202
  table.focus()
174
203
  else:
175
204
  return
176
205
 
177
206
  if table.loaded_rows == 0:
178
- table._setup_table()
207
+ table.setup_table()
179
208
 
180
209
  def action_toggle_help_panel(self) -> None:
181
210
  """Toggle the help panel on or off.
182
211
 
183
212
  Shows or hides the context-sensitive help panel. Creates it on first use.
184
-
185
- Returns:
186
- None
187
213
  """
188
214
  if self.help_panel:
189
215
  self.help_panel.display = not self.help_panel.display
@@ -191,44 +217,83 @@ class DataFrameViewer(App):
191
217
  self.help_panel = DataFrameHelpPanel()
192
218
  self.mount(self.help_panel)
193
219
 
194
- def action_add_tab(self) -> None:
220
+ def action_open_file(self) -> None:
195
221
  """Open file browser to load a file in a new tab.
196
222
 
197
223
  Displays the file open dialog for the user to select a file to load
198
224
  as a new tab in the interface.
225
+ """
226
+ self.push_screen(OpenFileScreen(), self.do_open_file)
199
227
 
200
- Returns:
201
- None
228
+ def action_close_tab(self) -> None:
229
+ """Close the current tab.
230
+
231
+ Checks for unsaved changes and prompts the user to save if needed.
232
+ If this is the last tab, exits the app.
202
233
  """
203
- self.push_screen(OpenFileScreen(), self._do_add_tab)
234
+ self.do_close_tab()
204
235
 
205
- def action_save_all_tabs(self) -> None:
206
- """Save all open tabs to a single Excel file.
236
+ def action_close_all_tabs(self) -> None:
237
+ """Close all tabs and exit the app.
207
238
 
208
- Displays a save dialog to choose filename and location, then saves all
209
- open tabs as separate sheets in a single Excel workbook.
239
+ Checks if any tabs have unsaved changes. If yes, opens a confirmation dialog.
240
+ Otherwise, quits immediately.
241
+ """
242
+ self.do_close_all_tabs()
210
243
 
211
- Returns:
212
- None
244
+ def action_save_current_tab(self) -> None:
245
+ """Save the currently active tab to file.
246
+
247
+ Opens the save dialog for the active tab's DataFrameTable to save its data.
213
248
  """
214
- callback = partial(self._get_active_table()._do_save_file, all_tabs=True)
215
- self.push_screen(
216
- SaveFileScreen("all-tabs.xlsx", title="Save All Tabs"),
217
- callback=callback,
218
- )
249
+ if table := self.get_active_table():
250
+ table.do_save_to_file(title="Save Current Tab", all_tabs=False)
219
251
 
220
- def action_close_tab(self) -> None:
221
- """Close the currently active tab.
252
+ def action_save_all_tabs(self) -> None:
253
+ """Save all open tabs to their respective files.
222
254
 
223
- Closes the current tab. If this is the only tab, exits the application instead.
255
+ Iterates through all DataFrameTable widgets and opens the save dialog for each.
256
+ """
257
+ if table := self.get_active_table():
258
+ table.do_save_to_file(title="Save All Tabs", all_tabs=True)
224
259
 
225
- Returns:
226
- None
260
+ def action_duplicate_tab(self) -> None:
261
+ """Duplicate the currently active tab.
262
+
263
+ Creates a copy of the current tab with the same data and filename.
264
+ The new tab is named with '_copy' suffix and inserted after the current tab.
227
265
  """
228
- if len(self.tabs) <= 1:
229
- self.app.exit()
266
+ if not (table := self.get_active_table()):
230
267
  return
231
- self._close_tab()
268
+
269
+ # Get current tab info
270
+ current_tabname = table.tabname
271
+ new_tabname = f"{current_tabname}_copy"
272
+ new_tabname = self.get_unique_tabname(new_tabname)
273
+
274
+ # Create new table with the same dataframe and filename
275
+ new_table = DataFrameTable(
276
+ table.df,
277
+ table.filename,
278
+ tabname=new_tabname,
279
+ zebra_stripes=True,
280
+ id=f"tab-{len(self.tabs) + 1}",
281
+ )
282
+ new_pane = TabPane(new_tabname, new_table, id=new_table.id)
283
+
284
+ # Add the new tab
285
+ active_pane = self.tabbed.active_pane
286
+ self.tabbed.add_pane(new_pane, after=active_pane)
287
+ self.tabs[new_pane] = new_table
288
+
289
+ # Show tab bar if needed
290
+ if len(self.tabs) > 1:
291
+ self.query_one(ContentTabs).display = True
292
+
293
+ # Activate and focus the new tab
294
+ self.tabbed.active = new_pane.id
295
+ new_table.focus()
296
+ new_table.dirty = True # Mark as dirty since it's a new unsaved tab
232
297
 
233
298
  def action_next_tab(self, offset: int = 1) -> None:
234
299
  """Switch to the next tab or previous tab.
@@ -238,9 +303,6 @@ class DataFrameViewer(App):
238
303
 
239
304
  Args:
240
305
  offset: Number of tabs to advance (+1 for next, -1 for previous). Defaults to 1.
241
-
242
- Returns:
243
- None
244
306
  """
245
307
  if len(self.tabs) <= 1:
246
308
  return
@@ -256,16 +318,13 @@ class DataFrameViewer(App):
256
318
 
257
319
  Shows or hides the tab bar at the bottom of the window. Useful for maximizing
258
320
  screen space in single-tab mode.
259
-
260
- Returns:
261
- None
262
321
  """
263
322
  tabs = self.query_one(ContentTabs)
264
323
  tabs.display = not tabs.display
265
324
  # status = "shown" if tabs.display else "hidden"
266
325
  # self.notify(f"Tab bar [$success]{status}[/]", title="Toggle")
267
326
 
268
- def _get_active_table(self) -> DataFrameTable | None:
327
+ def get_active_table(self) -> DataFrameTable | None:
269
328
  """Get the currently active DataFrameTable widget.
270
329
 
271
330
  Retrieves the table from the currently active tab. Returns None if no
@@ -282,23 +341,40 @@ class DataFrameViewer(App):
282
341
  self.notify("No active table found", title="Locate", severity="error")
283
342
  return None
284
343
 
285
- def _do_add_tab(self, filename: str) -> None:
286
- """Add a tab for the opened file.
344
+ def get_unique_tabname(self, tab_name: str) -> str:
345
+ """Generate a unique tab name based on the given base name.
346
+
347
+ If the base name already exists among current tabs, appends an index
348
+ to make it unique.
349
+
350
+ Args:
351
+ tab_name: The desired base name for the tab.
352
+
353
+ Returns:
354
+ A unique tab name.
355
+ """
356
+ tabname = tab_name
357
+ counter = 1
358
+ while any(table.tabname == tabname for table in self.tabs.values()):
359
+ tabname = f"{tab_name}_{counter}"
360
+ counter += 1
361
+
362
+ return tabname
363
+
364
+ def do_open_file(self, filename: str) -> None:
365
+ """Open a file.
287
366
 
288
367
  Loads the specified file and creates one or more tabs for it. For Excel files,
289
368
  creates one tab per sheet. For other formats, creates a single tab.
290
369
 
291
370
  Args:
292
371
  filename: Path to the file to load and add as tab(s).
293
-
294
- Returns:
295
- None
296
372
  """
297
373
  if filename and os.path.exists(filename):
298
374
  try:
299
375
  n_tab = 0
300
- for lf, filename, tabname in load_file(filename, prefix_sheet=True):
301
- self._add_tab(lf, filename, tabname)
376
+ for source in load_file(filename, prefix_sheet=True):
377
+ self.add_tab(source.frame, filename, source.tabname, after=self.tabbed.active_pane)
302
378
  n_tab += 1
303
379
  # self.notify(f"Added [$accent]{n_tab}[/] tab(s) for [$success]{filename}[/]", title="Open")
304
380
  except Exception as e:
@@ -306,7 +382,14 @@ class DataFrameViewer(App):
306
382
  else:
307
383
  self.notify(f"File does not exist: [$warning]{filename}[/]", title="Open", severity="warning")
308
384
 
309
- def _add_tab(self, df: pl.DataFrame, filename: str, tabname: str) -> None:
385
+ def add_tab(
386
+ self,
387
+ df: pl.DataFrame,
388
+ filename: str,
389
+ tabname: str,
390
+ before: TabPane | str | None = None,
391
+ after: TabPane | str | None = None,
392
+ ) -> None:
310
393
  """Add new tab for the given DataFrame.
311
394
 
312
395
  Creates and adds a new tab with the provided DataFrame and configuration.
@@ -317,30 +400,40 @@ class DataFrameViewer(App):
317
400
  lf: The Polars DataFrame to display in the new tab.
318
401
  filename: The source filename for this data (used in table metadata).
319
402
  tabname: The display name for the tab.
320
-
321
- Returns:
322
- None
323
403
  """
324
- # Ensure unique tab names
325
- counter = 1
326
- while any(tab.name == tabname for tab in self.tabs):
327
- tabname = f"{tabname}_{counter}"
328
- counter += 1
404
+ tabname = self.get_unique_tabname(tabname)
329
405
 
330
406
  # Find an available tab index
331
- tab_idx = f"tab_{len(self.tabs) + 1}"
407
+ tab_idx = f"tab-{len(self.tabs) + 1}"
332
408
  for idx in range(len(self.tabs)):
333
- pending_tab_idx = f"tab_{idx + 1}"
409
+ pending_tab_idx = f"tab-{idx + 1}"
334
410
  if any(tab.id == pending_tab_idx for tab in self.tabs):
335
411
  continue
336
412
 
337
413
  tab_idx = pending_tab_idx
338
414
  break
339
415
 
340
- table = DataFrameTable(df, filename, zebra_stripes=True, id=tab_idx, name=tabname)
341
- tab = TabPane(tabname, table, name=tabname, id=tab_idx)
342
- self.tabbed.add_pane(tab)
343
- self.tabs[tab] = table
416
+ table = DataFrameTable(df, filename, tabname=tabname, zebra_stripes=True, id=tab_idx)
417
+ tab = TabPane(tabname, table, id=tab_idx)
418
+ self.tabbed.add_pane(tab, before=before, after=after)
419
+
420
+ # Insert tab at specified position
421
+ tabs = list(self.tabs.keys())
422
+
423
+ if before and (idx := tabs.index(before)) != -1:
424
+ self.tabs = {
425
+ **{tab: self.tabs[tab] for tab in tabs[:idx]},
426
+ tab: table,
427
+ **{tab: self.tabs[tab] for tab in tabs[idx:]},
428
+ }
429
+ elif after and (idx := tabs.index(after)) != -1:
430
+ self.tabs = {
431
+ **{tab: self.tabs[tab] for tab in tabs[: idx + 1]},
432
+ tab: table,
433
+ **{tab: self.tabs[tab] for tab in tabs[idx + 1 :]},
434
+ }
435
+ else:
436
+ self.tabs[tab] = table
344
437
 
345
438
  if len(self.tabs) > 1:
346
439
  self.query_one(ContentTabs).display = True
@@ -349,22 +442,146 @@ class DataFrameViewer(App):
349
442
  self.tabbed.active = tab.id
350
443
  table.focus()
351
444
 
352
- def _close_tab(self) -> None:
445
+ def do_close_tab(self) -> None:
353
446
  """Close the currently active tab.
354
447
 
355
448
  Removes the active tab from the interface. If only one tab remains and no more
356
449
  can be closed, the application exits instead.
357
-
358
- Returns:
359
- None
360
450
  """
361
451
  try:
362
- if len(self.tabs) == 1:
363
- self.app.exit()
452
+ if not (active_pane := self.tabbed.active_pane):
453
+ return
454
+
455
+ if not (active_table := self.tabs.get(active_pane)):
456
+ return
457
+
458
+ def _on_save_confirm(result: bool) -> None:
459
+ """Handle the "save before closing?" confirmation."""
460
+ if result:
461
+ # User wants to save - close after save dialog opens
462
+ active_table.do_save_to_file(title="Save Current Tab", task_after_save="close_tab")
463
+ elif result is None:
464
+ # User cancelled - do nothing
465
+ return
466
+ else:
467
+ # User wants to discard - close immediately
468
+ self.close_tab()
469
+
470
+ if active_table.dirty:
471
+ self.push_screen(
472
+ ConfirmScreen(
473
+ "Close Tab",
474
+ label="This tab has unsaved changes. Save changes?",
475
+ yes="Save",
476
+ maybe="Discard",
477
+ no="Cancel",
478
+ ),
479
+ callback=_on_save_confirm,
480
+ )
364
481
  else:
365
- if active_pane := self.tabbed.active_pane:
366
- self.tabbed.remove_pane(active_pane.id)
367
- self.tabs.pop(active_pane)
368
- # self.notify(f"Closed tab [$success]{active_pane.name}[/]", title="Close")
369
- except NoMatches:
482
+ # No unsaved changes - close immediately
483
+ self.close_tab()
484
+ except Exception:
485
+ pass
486
+
487
+ def close_tab(self) -> None:
488
+ """Actually close the tab."""
489
+ try:
490
+ if not (active_pane := self.tabbed.active_pane):
491
+ return
492
+
493
+ self.tabbed.remove_pane(active_pane.id)
494
+ self.tabs.pop(active_pane)
495
+
496
+ # Quit app if no tabs remain
497
+ if len(self.tabs) == 0:
498
+ self.exit()
499
+ except Exception:
500
+ pass
501
+
502
+ def do_close_all_tabs(self) -> None:
503
+ """Close all tabs and quit the app.
504
+
505
+ Checks if any tabs have unsaved changes. If yes, opens a confirmation dialog.
506
+ Otherwise, quits immediately.
507
+ """
508
+ try:
509
+ # Check for dirty tabs
510
+ dirty_tabnames = [table.tabname for table in self.tabs.values() if table.dirty]
511
+ if not dirty_tabnames:
512
+ self.exit()
513
+ return
514
+
515
+ def _save_and_quit(result: bool) -> None:
516
+ if result:
517
+ self.get_active_table()._save_to_file(task_after_save="quit_app")
518
+ elif result is None:
519
+ # User cancelled - do nothing
520
+ return
521
+ else:
522
+ # User wants to discard - quit immediately
523
+ self.exit()
524
+
525
+ tab_list = "\n".join(f" - [$warning]{name}[/]" for name in dirty_tabnames)
526
+ label = (
527
+ f"The following tabs have unsaved changes:\n\n{tab_list}\n\nSave all changes?"
528
+ if len(dirty_tabnames) > 1
529
+ else f"The tab [$warning]{dirty_tabnames[0]}[/] has unsaved changes.\n\nSave changes?"
530
+ )
531
+ self.push_screen(
532
+ ConfirmScreen(
533
+ "Close All Tabs",
534
+ label=label,
535
+ yes="Save",
536
+ maybe="Discard",
537
+ no="Cancel",
538
+ ),
539
+ callback=_save_and_quit,
540
+ )
541
+
542
+ except Exception as e:
543
+ self.log(f"Error quitting all tabs: {str(e)}")
370
544
  pass
545
+
546
+ def do_rename_tab(self, content_tab: ContentTab) -> None:
547
+ """Open the rename tab screen.
548
+
549
+ Allows the user to rename the current tab and updates the table name accordingly.
550
+
551
+ Args:
552
+ content_tab: The ContentTab to rename.
553
+ """
554
+ if content_tab is None:
555
+ return
556
+
557
+ # Get list of existing tab names (excluding current tab)
558
+ existing_tabs = self.tabs.keys()
559
+
560
+ # Push the rename screen
561
+ self.push_screen(
562
+ RenameTabScreen(content_tab, existing_tabs),
563
+ callback=self.rename_tab,
564
+ )
565
+
566
+ def rename_tab(self, result) -> None:
567
+ """Handle result from RenameTabScreen."""
568
+ if result is None:
569
+ return
570
+
571
+ content_tab: ContentTab
572
+ content_tab, new_name = result
573
+
574
+ # Update the tab name
575
+ old_name = content_tab.label_text
576
+ content_tab.label = new_name
577
+
578
+ # Mark tab as dirty to indicate name change
579
+ tab_id = content_tab.id.removeprefix("--content-tab-")
580
+ for tab, table in self.tabs.items():
581
+ if tab.id == tab_id:
582
+ table.tabname = new_name
583
+ table.dirty = True
584
+ table.focus()
585
+ break
586
+
587
+ self.notify(f"Renamed tab [$accent]{old_name}[/] to [$success]{new_name}[/]", title="Rename")