@@ -387,40 +387,45 @@ extension Ghostty {
387
387
388
388
/// Set the title by prompting the user.
389
389
func promptTitle( ) {
390
- // Create an alert dialog
391
- let alert = NSAlert ( )
392
- alert. messageText = " Change Terminal Title "
393
- alert. informativeText = " Leave blank to restore the default. "
394
- alert. alertStyle = . informational
395
-
396
- // Add a text field to the alert
397
- let textField = NSTextField ( frame: NSRect ( x: 0 , y: 0 , width: 250 , height: 24 ) )
398
- textField. stringValue = title
399
- alert. accessoryView = textField
400
-
401
- // Add buttons
402
- alert. addButton ( withTitle: " OK " )
403
- alert. addButton ( withTitle: " Cancel " )
404
-
405
- let response = alert. runModal ( )
406
-
407
- // Check if the user clicked "OK"
408
- if response == . alertFirstButtonReturn {
409
- // Get the input text
410
- let newTitle = textField. stringValue
411
-
412
- if newTitle. isEmpty {
413
- // Empty means that user wants the title to be set automatically
414
- // We also need to reload the config for the "title" property to be
415
- // used again by this tab.
416
- let prevTitle = titleFromTerminal ?? " 👻 "
417
- titleFromTerminal = nil
418
- setTitle ( prevTitle)
419
- } else {
420
- // Set the title and prevent it from being changed automatically
421
- titleFromTerminal = title
422
- title = newTitle
390
+ // Create a popover
391
+ let hostingController = NSHostingController ( rootView: TabTitleEditPopover (
392
+ currentTitle: title,
393
+ onComplete: { [ weak self] newTitle in
394
+ if newTitle. isEmpty {
395
+ // Empty means that user wants the title to be set automatically
396
+ let prevTitle = self ? . titleFromTerminal ?? " 👻 "
397
+ self ? . titleFromTerminal = nil
398
+ self ? . setTitle ( prevTitle)
399
+ } else {
400
+ // Set the title and prevent it from being changed automatically
401
+ self ? . titleFromTerminal = self ? . title
402
+ self ? . title = newTitle
403
+ }
423
404
}
405
+ ) )
406
+
407
+ let popover = NSPopover ( )
408
+ popover. contentViewController = hostingController
409
+ popover. behavior = . transient
410
+
411
+ // Show the popover below the current tab title
412
+ if let window = self . window as? TerminalWindow ,
413
+ let toolbar = window. toolbar as? TerminalToolbar ,
414
+ let titleItem = toolbar. items. first ( where: { $0. itemIdentifier == . titleText } ) ,
415
+ let titleView = titleItem. view {
416
+ popover. show (
417
+ relativeTo: titleView. bounds,
418
+ of: titleView,
419
+ preferredEdge: . maxY
420
+ )
421
+ } else if let window = self . window,
422
+ let titlebarView = window. contentView? . superview? . firstDescendant ( withClassName: " NSTitlebarView " ) ,
423
+ let titleView = titlebarView. firstDescendant ( withClassName: " NSTextField " ) {
424
+ popover. show (
425
+ relativeTo: titleView. bounds,
426
+ of: titleView,
427
+ preferredEdge: . maxY
428
+ )
424
429
}
425
430
}
426
431
@@ -1240,7 +1245,7 @@ extension Ghostty {
1240
1245
AppDelegate . logger. warning ( " action failed action= \( action) " )
1241
1246
}
1242
1247
}
1243
-
1248
+
1244
1249
@IBAction func changeTitle( _ sender: Any ) {
1245
1250
promptTitle ( )
1246
1251
}
@@ -1607,3 +1612,47 @@ extension Ghostty.SurfaceView {
1607
1612
return false
1608
1613
}
1609
1614
}
1615
+
1616
+ struct TabTitleEditPopover : View {
1617
+ @Environment ( \. dismiss) private var dismiss
1618
+ @State private var newTitle : String
1619
+ let currentTitle : String
1620
+ let onComplete : ( String ) -> Void
1621
+
1622
+ init ( currentTitle: String , onComplete: @escaping ( String ) -> Void ) {
1623
+ self . currentTitle = currentTitle
1624
+ self . _newTitle = State ( initialValue: currentTitle)
1625
+ self . onComplete = onComplete
1626
+ }
1627
+
1628
+ var body : some View {
1629
+ VStack ( alignment: . leading, spacing: 8 ) {
1630
+ TextField ( " Enter title (leave empty for default) " , text: $newTitle)
1631
+ . textFieldStyle ( RoundedBorderTextFieldStyle ( ) )
1632
+ . onSubmit ( submit)
1633
+
1634
+ HStack ( spacing: 8 ) {
1635
+ Button ( action: { dismiss ( ) } ) {
1636
+ Text ( " Cancel " )
1637
+ . frame ( maxWidth: . infinity)
1638
+ }
1639
+ . buttonStyle ( . bordered)
1640
+ . keyboardShortcut ( . cancelAction)
1641
+
1642
+ Button ( action: { submit ( ) } ) {
1643
+ Text ( " OK " )
1644
+ . frame ( maxWidth: . infinity)
1645
+ }
1646
+ . buttonStyle ( . borderedProminent)
1647
+ . keyboardShortcut ( . defaultAction)
1648
+ }
1649
+ . fixedSize ( horizontal: false , vertical: true )
1650
+ }
1651
+ . padding ( )
1652
+ }
1653
+
1654
+ private func submit( ) {
1655
+ onComplete ( newTitle)
1656
+ dismiss ( )
1657
+ }
1658
+ }
0 commit comments