dataframe-textual 1.5.0__py3-none-any.whl → 1.10.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.
@@ -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,9 @@ 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
+ }
74
84
  """
75
85
 
76
86
  def __init__(self, *sources: Source) -> None:
@@ -81,9 +91,6 @@ class DataFrameViewer(App):
81
91
  Args:
82
92
  sources: sources to load dataframes from, each as a tuple of
83
93
  (DataFrame, filename, tabname).
84
-
85
- Returns:
86
- None
87
94
  """
88
95
  super().__init__()
89
96
  self.sources = sources
@@ -105,7 +112,7 @@ class DataFrameViewer(App):
105
112
  seen_names = set()
106
113
  for idx, source in enumerate(self.sources, start=1):
107
114
  df, filename, tabname = source.frame, source.filename, source.tabname
108
- tab_id = f"tab_{idx}"
115
+ tab_id = f"tab-{idx}"
109
116
 
110
117
  if not tabname:
111
118
  tabname = Path(filename).stem or tab_id
@@ -118,8 +125,8 @@ class DataFrameViewer(App):
118
125
  seen_names.add(tabname)
119
126
 
120
127
  try:
121
- table = DataFrameTable(df, filename, name=tabname, id=tab_id, zebra_stripes=True)
122
- tab = TabPane(tabname, table, name=tabname, id=tab_id)
128
+ table = DataFrameTable(df, filename, tabname=tabname, id=tab_id, zebra_stripes=True)
129
+ tab = TabPane(tabname, table, id=tab_id)
123
130
  self.tabs[tab] = table
124
131
  yield tab
125
132
  except Exception as e:
@@ -134,13 +141,15 @@ class DataFrameViewer(App):
134
141
 
135
142
  Initializes the app by hiding the tab bar for single-file mode and focusing
136
143
  the active table widget.
137
-
138
- Returns:
139
- None
140
144
  """
141
145
  if len(self.tabs) == 1:
142
146
  self.query_one(ContentTabs).display = False
143
- self._get_active_table().focus()
147
+ self.get_active_table().focus()
148
+
149
+ def on_ready(self) -> None:
150
+ """Called when the app is ready."""
151
+ # self.log(self.tree)
152
+ pass
144
153
 
145
154
  def on_key(self, event) -> None:
146
155
  """Handle key press events at the application level.
@@ -149,14 +158,32 @@ class DataFrameViewer(App):
149
158
 
150
159
  Args:
151
160
  event: The key event object containing key information.
152
-
153
- Returns:
154
- None
155
161
  """
156
162
  if event.key == "k":
157
163
  self.theme = get_next_item(list(BUILTIN_THEMES.keys()), self.theme)
158
164
  self.notify(f"Switched to theme: [$success]{self.theme}[/]", title="Theme")
159
165
 
166
+ def on_click(self, event: Click) -> None:
167
+ """Handle mouse click events on tabs.
168
+
169
+ Detects double-clicks on tab headers and opens the rename screen.
170
+
171
+ Args:
172
+ event: The click event containing position information.
173
+ """
174
+ # Check if this is a double-click (chain > 1) on a tab header
175
+ if event.chain > 1:
176
+ try:
177
+ # Get the widget that was clicked
178
+ content_tab = event.widget
179
+
180
+ # Check if it's a ContentTab (tab header)
181
+ if isinstance(content_tab, ContentTab):
182
+ self.do_rename_tab(content_tab)
183
+ except Exception as e:
184
+ self.log(f"Error handling tab rename click: {str(e)}")
185
+ pass
186
+
160
187
  def on_tabbed_content_tab_activated(self, event: TabbedContent.TabActivated) -> None:
161
188
  """Handle tab activation events.
162
189
 
@@ -165,26 +192,20 @@ class DataFrameViewer(App):
165
192
 
166
193
  Args:
167
194
  event: The tab activated event containing the activated tab pane.
168
-
169
- Returns:
170
- None
171
195
  """
172
196
  # Focus the table in the newly activated tab
173
- if table := self._get_active_table():
197
+ if table := self.get_active_table():
174
198
  table.focus()
175
199
  else:
176
200
  return
177
201
 
178
202
  if table.loaded_rows == 0:
179
- table._setup_table()
203
+ table.setup_table()
180
204
 
181
205
  def action_toggle_help_panel(self) -> None:
182
206
  """Toggle the help panel on or off.
183
207
 
184
208
  Shows or hides the context-sensitive help panel. Creates it on first use.
185
-
186
- Returns:
187
- None
188
209
  """
189
210
  if self.help_panel:
190
211
  self.help_panel.display = not self.help_panel.display
@@ -192,44 +213,83 @@ class DataFrameViewer(App):
192
213
  self.help_panel = DataFrameHelpPanel()
193
214
  self.mount(self.help_panel)
194
215
 
195
- def action_add_tab(self) -> None:
216
+ def action_open_file(self) -> None:
196
217
  """Open file browser to load a file in a new tab.
197
218
 
198
219
  Displays the file open dialog for the user to select a file to load
199
220
  as a new tab in the interface.
221
+ """
222
+ self.push_screen(OpenFileScreen(), self.do_open_file)
200
223
 
201
- Returns:
202
- None
224
+ def action_close_tab(self) -> None:
225
+ """Close the current tab.
226
+
227
+ Checks for unsaved changes and prompts the user to save if needed.
228
+ If this is the last tab, exits the app.
203
229
  """
204
- self.push_screen(OpenFileScreen(), self._do_add_tab)
230
+ self.do_close_tab()
205
231
 
206
- def action_save_all_tabs(self) -> None:
207
- """Save all open tabs to a single Excel file.
232
+ def action_close_all_tabs(self) -> None:
233
+ """Close all tabs and exit the app.
208
234
 
209
- Displays a save dialog to choose filename and location, then saves all
210
- open tabs as separate sheets in a single Excel workbook.
235
+ Checks if any tabs have unsaved changes. If yes, opens a confirmation dialog.
236
+ Otherwise, quits immediately.
237
+ """
238
+ self.do_close_all_tabs()
211
239
 
212
- Returns:
213
- None
240
+ def action_save_current_tab(self) -> None:
241
+ """Save the currently active tab to file.
242
+
243
+ Opens the save dialog for the active tab's DataFrameTable to save its data.
214
244
  """
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
- )
245
+ if table := self.get_active_table():
246
+ table.do_save_to_file(title="Save Current Tab", all_tabs=False)
220
247
 
221
- def action_close_tab(self) -> None:
222
- """Close the currently active tab.
248
+ def action_save_all_tabs(self) -> None:
249
+ """Save all open tabs to their respective files.
250
+
251
+ Iterates through all DataFrameTable widgets and opens the save dialog for each.
252
+ """
253
+ if table := self.get_active_table():
254
+ table.do_save_to_file(title="Save All Tabs", all_tabs=True)
223
255
 
224
- Closes the current tab. If this is the only tab, exits the application instead.
256
+ def action_duplicate_tab(self) -> None:
257
+ """Duplicate the currently active tab.
225
258
 
226
- Returns:
227
- None
259
+ Creates a copy of the current tab with the same data and filename.
260
+ The new tab is named with '_copy' suffix and inserted after the current tab.
228
261
  """
229
- if len(self.tabs) <= 1:
230
- self.app.exit()
262
+ if not (table := self.get_active_table()):
231
263
  return
232
- self._close_tab()
264
+
265
+ # Get current tab info
266
+ current_tabname = table.tabname
267
+ new_tabname = f"{current_tabname}_copy"
268
+ new_tabname = self.get_unique_tabname(new_tabname)
269
+
270
+ # Create new table with the same dataframe and filename
271
+ new_table = DataFrameTable(
272
+ table.df,
273
+ table.filename,
274
+ tabname=new_tabname,
275
+ zebra_stripes=True,
276
+ id=f"tab-{len(self.tabs) + 1}",
277
+ )
278
+ new_pane = TabPane(new_tabname, new_table, id=new_table.id)
279
+
280
+ # Add the new tab
281
+ active_pane = self.tabbed.active_pane
282
+ self.tabbed.add_pane(new_pane, after=active_pane)
283
+ self.tabs[new_pane] = new_table
284
+
285
+ # Show tab bar if needed
286
+ if len(self.tabs) > 1:
287
+ self.query_one(ContentTabs).display = True
288
+
289
+ # Activate and focus the new tab
290
+ self.tabbed.active = new_pane.id
291
+ new_table.focus()
292
+ new_table.dirty = True # Mark as dirty since it's a new unsaved tab
233
293
 
234
294
  def action_next_tab(self, offset: int = 1) -> None:
235
295
  """Switch to the next tab or previous tab.
@@ -239,9 +299,6 @@ class DataFrameViewer(App):
239
299
 
240
300
  Args:
241
301
  offset: Number of tabs to advance (+1 for next, -1 for previous). Defaults to 1.
242
-
243
- Returns:
244
- None
245
302
  """
246
303
  if len(self.tabs) <= 1:
247
304
  return
@@ -257,16 +314,13 @@ class DataFrameViewer(App):
257
314
 
258
315
  Shows or hides the tab bar at the bottom of the window. Useful for maximizing
259
316
  screen space in single-tab mode.
260
-
261
- Returns:
262
- None
263
317
  """
264
318
  tabs = self.query_one(ContentTabs)
265
319
  tabs.display = not tabs.display
266
320
  # status = "shown" if tabs.display else "hidden"
267
321
  # self.notify(f"Tab bar [$success]{status}[/]", title="Toggle")
268
322
 
269
- def _get_active_table(self) -> DataFrameTable | None:
323
+ def get_active_table(self) -> DataFrameTable | None:
270
324
  """Get the currently active DataFrameTable widget.
271
325
 
272
326
  Retrieves the table from the currently active tab. Returns None if no
@@ -283,23 +337,40 @@ class DataFrameViewer(App):
283
337
  self.notify("No active table found", title="Locate", severity="error")
284
338
  return None
285
339
 
286
- def _do_add_tab(self, filename: str) -> None:
287
- """Add a tab for the opened file.
340
+ def get_unique_tabname(self, tab_name: str) -> str:
341
+ """Generate a unique tab name based on the given base name.
342
+
343
+ If the base name already exists among current tabs, appends an index
344
+ to make it unique.
345
+
346
+ Args:
347
+ tab_name: The desired base name for the tab.
348
+
349
+ Returns:
350
+ A unique tab name.
351
+ """
352
+ tabname = tab_name
353
+ counter = 1
354
+ while any(table.tabname == tabname for table in self.tabs.values()):
355
+ tabname = f"{tab_name}_{counter}"
356
+ counter += 1
357
+
358
+ return tabname
359
+
360
+ def do_open_file(self, filename: str) -> None:
361
+ """Open a file.
288
362
 
289
363
  Loads the specified file and creates one or more tabs for it. For Excel files,
290
364
  creates one tab per sheet. For other formats, creates a single tab.
291
365
 
292
366
  Args:
293
367
  filename: Path to the file to load and add as tab(s).
294
-
295
- Returns:
296
- None
297
368
  """
298
369
  if filename and os.path.exists(filename):
299
370
  try:
300
371
  n_tab = 0
301
- for lf, filename, tabname in load_file(filename, prefix_sheet=True):
302
- self._add_tab(lf, filename, tabname)
372
+ for source in load_file(filename, prefix_sheet=True):
373
+ self.add_tab(source.frame, filename, source.tabname, after=self.tabbed.active_pane)
303
374
  n_tab += 1
304
375
  # self.notify(f"Added [$accent]{n_tab}[/] tab(s) for [$success]{filename}[/]", title="Open")
305
376
  except Exception as e:
@@ -307,7 +378,14 @@ class DataFrameViewer(App):
307
378
  else:
308
379
  self.notify(f"File does not exist: [$warning]{filename}[/]", title="Open", severity="warning")
309
380
 
310
- def _add_tab(self, df: pl.DataFrame, filename: str, tabname: str) -> None:
381
+ def add_tab(
382
+ self,
383
+ df: pl.DataFrame,
384
+ filename: str,
385
+ tabname: str,
386
+ before: TabPane | str | None = None,
387
+ after: TabPane | str | None = None,
388
+ ) -> None:
311
389
  """Add new tab for the given DataFrame.
312
390
 
313
391
  Creates and adds a new tab with the provided DataFrame and configuration.
@@ -318,30 +396,40 @@ class DataFrameViewer(App):
318
396
  lf: The Polars DataFrame to display in the new tab.
319
397
  filename: The source filename for this data (used in table metadata).
320
398
  tabname: The display name for the tab.
321
-
322
- Returns:
323
- None
324
399
  """
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
400
+ tabname = self.get_unique_tabname(tabname)
330
401
 
331
402
  # Find an available tab index
332
- tab_idx = f"tab_{len(self.tabs) + 1}"
403
+ tab_idx = f"tab-{len(self.tabs) + 1}"
333
404
  for idx in range(len(self.tabs)):
334
- pending_tab_idx = f"tab_{idx + 1}"
405
+ pending_tab_idx = f"tab-{idx + 1}"
335
406
  if any(tab.id == pending_tab_idx for tab in self.tabs):
336
407
  continue
337
408
 
338
409
  tab_idx = pending_tab_idx
339
410
  break
340
411
 
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
412
+ table = DataFrameTable(df, filename, tabname=tabname, zebra_stripes=True, id=tab_idx)
413
+ tab = TabPane(tabname, table, id=tab_idx)
414
+ self.tabbed.add_pane(tab, before=before, after=after)
415
+
416
+ # Insert tab at specified position
417
+ tabs = list(self.tabs.keys())
418
+
419
+ if before and (idx := tabs.index(before)) != -1:
420
+ self.tabs = {
421
+ **{tab: self.tabs[tab] for tab in tabs[:idx]},
422
+ tab: table,
423
+ **{tab: self.tabs[tab] for tab in tabs[idx:]},
424
+ }
425
+ elif after and (idx := tabs.index(after)) != -1:
426
+ self.tabs = {
427
+ **{tab: self.tabs[tab] for tab in tabs[: idx + 1]},
428
+ tab: table,
429
+ **{tab: self.tabs[tab] for tab in tabs[idx + 1 :]},
430
+ }
431
+ else:
432
+ self.tabs[tab] = table
345
433
 
346
434
  if len(self.tabs) > 1:
347
435
  self.query_one(ContentTabs).display = True
@@ -350,22 +438,146 @@ class DataFrameViewer(App):
350
438
  self.tabbed.active = tab.id
351
439
  table.focus()
352
440
 
353
- def _close_tab(self) -> None:
441
+ def do_close_tab(self) -> None:
354
442
  """Close the currently active tab.
355
443
 
356
444
  Removes the active tab from the interface. If only one tab remains and no more
357
445
  can be closed, the application exits instead.
358
-
359
- Returns:
360
- None
361
446
  """
362
447
  try:
363
- if len(self.tabs) == 1:
364
- self.app.exit()
448
+ if not (active_pane := self.tabbed.active_pane):
449
+ return
450
+
451
+ if not (active_table := self.tabs.get(active_pane)):
452
+ return
453
+
454
+ def _on_save_confirm(result: bool) -> None:
455
+ """Handle the "save before closing?" confirmation."""
456
+ if result:
457
+ # User wants to save - close after save dialog opens
458
+ active_table.do_save_to_file(title="Save Current Tab", task_after_save="close_tab")
459
+ elif result is None:
460
+ # User cancelled - do nothing
461
+ return
462
+ else:
463
+ # User wants to discard - close immediately
464
+ self.close_tab()
465
+
466
+ if active_table.dirty:
467
+ self.push_screen(
468
+ ConfirmScreen(
469
+ "Close Tab",
470
+ label="This tab has unsaved changes. Save changes?",
471
+ yes="Save",
472
+ maybe="Discard",
473
+ no="Cancel",
474
+ ),
475
+ callback=_on_save_confirm,
476
+ )
365
477
  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:
478
+ # No unsaved changes - close immediately
479
+ self.close_tab()
480
+ except Exception:
371
481
  pass
482
+
483
+ def close_tab(self) -> None:
484
+ """Actually close the tab."""
485
+ try:
486
+ if not (active_pane := self.tabbed.active_pane):
487
+ return
488
+
489
+ self.tabbed.remove_pane(active_pane.id)
490
+ self.tabs.pop(active_pane)
491
+
492
+ # Quit app if no tabs remain
493
+ if len(self.tabs) == 0:
494
+ self.exit()
495
+ except Exception:
496
+ pass
497
+
498
+ def do_close_all_tabs(self) -> None:
499
+ """Close all tabs and quit the app.
500
+
501
+ Checks if any tabs have unsaved changes. If yes, opens a confirmation dialog.
502
+ Otherwise, quits immediately.
503
+ """
504
+ try:
505
+ # Check for dirty tabs
506
+ dirty_tabnames = [table.tabname for table in self.tabs.values() if table.dirty]
507
+ if not dirty_tabnames:
508
+ self.exit()
509
+ return
510
+
511
+ def _save_and_quit(result: bool) -> None:
512
+ if result:
513
+ self.get_active_table()._save_to_file(task_after_save="quit_app")
514
+ elif result is None:
515
+ # User cancelled - do nothing
516
+ return
517
+ else:
518
+ # User wants to discard - quit immediately
519
+ self.exit()
520
+
521
+ tab_list = "\n".join(f" - [$warning]{name}[/]" for name in dirty_tabnames)
522
+ label = (
523
+ f"The following tabs have unsaved changes:\n\n{tab_list}\n\nSave all changes?"
524
+ if len(dirty_tabnames) > 1
525
+ else f"The tab [$warning]{dirty_tabnames[0]}[/] has unsaved changes.\n\nSave changes?"
526
+ )
527
+ self.push_screen(
528
+ ConfirmScreen(
529
+ "Close All Tabs",
530
+ label=label,
531
+ yes="Save",
532
+ maybe="Discard",
533
+ no="Cancel",
534
+ ),
535
+ callback=_save_and_quit,
536
+ )
537
+
538
+ except Exception as e:
539
+ self.log(f"Error quitting all tabs: {str(e)}")
540
+ pass
541
+
542
+ def do_rename_tab(self, content_tab: ContentTab) -> None:
543
+ """Open the rename tab screen.
544
+
545
+ Allows the user to rename the current tab and updates the table name accordingly.
546
+
547
+ Args:
548
+ content_tab: The ContentTab to rename.
549
+ """
550
+ if content_tab is None:
551
+ return
552
+
553
+ # Get list of existing tab names (excluding current tab)
554
+ existing_tabs = self.tabs.keys()
555
+
556
+ # Push the rename screen
557
+ self.push_screen(
558
+ RenameTabScreen(content_tab, existing_tabs),
559
+ callback=self.rename_tab,
560
+ )
561
+
562
+ def rename_tab(self, result) -> None:
563
+ """Handle result from RenameTabScreen."""
564
+ if result is None:
565
+ return
566
+
567
+ content_tab: ContentTab
568
+ content_tab, new_name = result
569
+
570
+ # Update the tab name
571
+ old_name = content_tab.label_text
572
+ content_tab.label = new_name
573
+
574
+ # Mark tab as dirty to indicate name change
575
+ tab_id = content_tab.id.removeprefix("--content-tab-")
576
+ for tab, table in self.tabs.items():
577
+ if tab.id == tab_id:
578
+ table.tabname = new_name
579
+ table.dirty = True
580
+ table.focus()
581
+ break
582
+
583
+ self.notify(f"Renamed tab [$accent]{old_name}[/] to [$success]{new_name}[/]", title="Rename")