dataframe-textual 1.5.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
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,6 +78,13 @@ 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
90
  def __init__(self, *sources: Source) -> None:
@@ -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
@@ -105,7 +116,7 @@ class DataFrameViewer(App):
105
116
  seen_names = set()
106
117
  for idx, source in enumerate(self.sources, start=1):
107
118
  df, filename, tabname = source.frame, source.filename, source.tabname
108
- tab_id = f"tab_{idx}"
119
+ tab_id = f"tab-{idx}"
109
120
 
110
121
  if not tabname:
111
122
  tabname = Path(filename).stem or tab_id
@@ -118,8 +129,8 @@ class DataFrameViewer(App):
118
129
  seen_names.add(tabname)
119
130
 
120
131
  try:
121
- table = DataFrameTable(df, filename, name=tabname, id=tab_id, zebra_stripes=True)
122
- 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)
123
134
  self.tabs[tab] = table
124
135
  yield tab
125
136
  except Exception as e:
@@ -134,13 +145,15 @@ class DataFrameViewer(App):
134
145
 
135
146
  Initializes the app by hiding the tab bar for single-file mode and focusing
136
147
  the active table widget.
137
-
138
- Returns:
139
- None
140
148
  """
141
149
  if len(self.tabs) == 1:
142
150
  self.query_one(ContentTabs).display = False
143
- 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
144
157
 
145
158
  def on_key(self, event) -> None:
146
159
  """Handle key press events at the application level.
@@ -149,14 +162,32 @@ class DataFrameViewer(App):
149
162
 
150
163
  Args:
151
164
  event: The key event object containing key information.
152
-
153
- Returns:
154
- None
155
165
  """
156
166
  if event.key == "k":
157
167
  self.theme = get_next_item(list(BUILTIN_THEMES.keys()), self.theme)
158
168
  self.notify(f"Switched to theme: [$success]{self.theme}[/]", title="Theme")
159
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
+
160
191
  def on_tabbed_content_tab_activated(self, event: TabbedContent.TabActivated) -> None:
161
192
  """Handle tab activation events.
162
193
 
@@ -165,26 +196,20 @@ class DataFrameViewer(App):
165
196
 
166
197
  Args:
167
198
  event: The tab activated event containing the activated tab pane.
168
-
169
- Returns:
170
- None
171
199
  """
172
200
  # Focus the table in the newly activated tab
173
- if table := self._get_active_table():
201
+ if table := self.get_active_table():
174
202
  table.focus()
175
203
  else:
176
204
  return
177
205
 
178
206
  if table.loaded_rows == 0:
179
- table._setup_table()
207
+ table.setup_table()
180
208
 
181
209
  def action_toggle_help_panel(self) -> None:
182
210
  """Toggle the help panel on or off.
183
211
 
184
212
  Shows or hides the context-sensitive help panel. Creates it on first use.
185
-
186
- Returns:
187
- None
188
213
  """
189
214
  if self.help_panel:
190
215
  self.help_panel.display = not self.help_panel.display
@@ -192,44 +217,83 @@ class DataFrameViewer(App):
192
217
  self.help_panel = DataFrameHelpPanel()
193
218
  self.mount(self.help_panel)
194
219
 
195
- def action_add_tab(self) -> None:
220
+ def action_open_file(self) -> None:
196
221
  """Open file browser to load a file in a new tab.
197
222
 
198
223
  Displays the file open dialog for the user to select a file to load
199
224
  as a new tab in the interface.
225
+ """
226
+ self.push_screen(OpenFileScreen(), self.do_open_file)
200
227
 
201
- Returns:
202
- 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.
203
233
  """
204
- self.push_screen(OpenFileScreen(), self._do_add_tab)
234
+ self.do_close_tab()
205
235
 
206
- def action_save_all_tabs(self) -> None:
207
- """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.
208
238
 
209
- Displays a save dialog to choose filename and location, then saves all
210
- 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()
211
243
 
212
- Returns:
213
- 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.
214
248
  """
215
- callback = partial(self._get_active_table()._do_save_file, all_tabs=True)
216
- self.push_screen(
217
- SaveFileScreen("all-tabs.xlsx", title="Save All Tabs"),
218
- callback=callback,
219
- )
249
+ if table := self.get_active_table():
250
+ table.do_save_to_file(title="Save Current Tab", all_tabs=False)
220
251
 
221
- def action_close_tab(self) -> None:
222
- """Close the currently active tab.
252
+ def action_save_all_tabs(self) -> None:
253
+ """Save all open tabs to their respective files.
223
254
 
224
- 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)
225
259
 
226
- Returns:
227
- 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.
228
265
  """
229
- if len(self.tabs) <= 1:
230
- self.app.exit()
266
+ if not (table := self.get_active_table()):
231
267
  return
232
- 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
233
297
 
234
298
  def action_next_tab(self, offset: int = 1) -> None:
235
299
  """Switch to the next tab or previous tab.
@@ -239,9 +303,6 @@ class DataFrameViewer(App):
239
303
 
240
304
  Args:
241
305
  offset: Number of tabs to advance (+1 for next, -1 for previous). Defaults to 1.
242
-
243
- Returns:
244
- None
245
306
  """
246
307
  if len(self.tabs) <= 1:
247
308
  return
@@ -257,16 +318,13 @@ class DataFrameViewer(App):
257
318
 
258
319
  Shows or hides the tab bar at the bottom of the window. Useful for maximizing
259
320
  screen space in single-tab mode.
260
-
261
- Returns:
262
- None
263
321
  """
264
322
  tabs = self.query_one(ContentTabs)
265
323
  tabs.display = not tabs.display
266
324
  # status = "shown" if tabs.display else "hidden"
267
325
  # self.notify(f"Tab bar [$success]{status}[/]", title="Toggle")
268
326
 
269
- def _get_active_table(self) -> DataFrameTable | None:
327
+ def get_active_table(self) -> DataFrameTable | None:
270
328
  """Get the currently active DataFrameTable widget.
271
329
 
272
330
  Retrieves the table from the currently active tab. Returns None if no
@@ -283,23 +341,40 @@ class DataFrameViewer(App):
283
341
  self.notify("No active table found", title="Locate", severity="error")
284
342
  return None
285
343
 
286
- def _do_add_tab(self, filename: str) -> None:
287
- """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.
288
366
 
289
367
  Loads the specified file and creates one or more tabs for it. For Excel files,
290
368
  creates one tab per sheet. For other formats, creates a single tab.
291
369
 
292
370
  Args:
293
371
  filename: Path to the file to load and add as tab(s).
294
-
295
- Returns:
296
- None
297
372
  """
298
373
  if filename and os.path.exists(filename):
299
374
  try:
300
375
  n_tab = 0
301
- for lf, filename, tabname in load_file(filename, prefix_sheet=True):
302
- 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)
303
378
  n_tab += 1
304
379
  # self.notify(f"Added [$accent]{n_tab}[/] tab(s) for [$success]{filename}[/]", title="Open")
305
380
  except Exception as e:
@@ -307,7 +382,14 @@ class DataFrameViewer(App):
307
382
  else:
308
383
  self.notify(f"File does not exist: [$warning]{filename}[/]", title="Open", severity="warning")
309
384
 
310
- 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:
311
393
  """Add new tab for the given DataFrame.
312
394
 
313
395
  Creates and adds a new tab with the provided DataFrame and configuration.
@@ -318,30 +400,40 @@ class DataFrameViewer(App):
318
400
  lf: The Polars DataFrame to display in the new tab.
319
401
  filename: The source filename for this data (used in table metadata).
320
402
  tabname: The display name for the tab.
321
-
322
- Returns:
323
- None
324
403
  """
325
- # Ensure unique tab names
326
- counter = 1
327
- while any(tab.name == tabname for tab in self.tabs):
328
- tabname = f"{tabname}_{counter}"
329
- counter += 1
404
+ tabname = self.get_unique_tabname(tabname)
330
405
 
331
406
  # Find an available tab index
332
- tab_idx = f"tab_{len(self.tabs) + 1}"
407
+ tab_idx = f"tab-{len(self.tabs) + 1}"
333
408
  for idx in range(len(self.tabs)):
334
- pending_tab_idx = f"tab_{idx + 1}"
409
+ pending_tab_idx = f"tab-{idx + 1}"
335
410
  if any(tab.id == pending_tab_idx for tab in self.tabs):
336
411
  continue
337
412
 
338
413
  tab_idx = pending_tab_idx
339
414
  break
340
415
 
341
- table = DataFrameTable(df, filename, zebra_stripes=True, id=tab_idx, name=tabname)
342
- tab = TabPane(tabname, table, name=tabname, id=tab_idx)
343
- self.tabbed.add_pane(tab)
344
- 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
345
437
 
346
438
  if len(self.tabs) > 1:
347
439
  self.query_one(ContentTabs).display = True
@@ -350,22 +442,146 @@ class DataFrameViewer(App):
350
442
  self.tabbed.active = tab.id
351
443
  table.focus()
352
444
 
353
- def _close_tab(self) -> None:
445
+ def do_close_tab(self) -> None:
354
446
  """Close the currently active tab.
355
447
 
356
448
  Removes the active tab from the interface. If only one tab remains and no more
357
449
  can be closed, the application exits instead.
358
-
359
- Returns:
360
- None
361
450
  """
362
451
  try:
363
- if len(self.tabs) == 1:
364
- 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
+ )
365
481
  else:
366
- if active_pane := self.tabbed.active_pane:
367
- self.tabbed.remove_pane(active_pane.id)
368
- self.tabs.pop(active_pane)
369
- # self.notify(f"Closed tab [$success]{active_pane.name}[/]", title="Close")
370
- 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)}")
371
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")