Files
EnhancedTerminal/MainWindowController.swift
Paul Bailey c42c1c7a2d Add keyboard shortcut to close split panes
- Cmd+Shift+W closes the active pane
- Automatically removes split view when only one pane remains
- Falls back to closing tab if no splits exist
- Updated keyboard shortcuts reference
2026-01-31 23:36:49 +00:00

727 lines
28 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Cocoa
class MainWindowController: NSWindowController {
var tabManager = TabManager()
var tabBar: NSStackView!
var contentView: NSView!
var splitView: NSSplitView?
var activePaneIndex: Int = 0
var panes: [TerminalSession] = []
var historyPanel: HistorySearchPanel!
var snippetPanel: SnippetPanel!
var aiPanel: AIGeneratorPanel!
var settingsWindow: SettingsWindowController!
convenience init() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 1000, height: 700),
styleMask: [.titled, .closable, .miniaturizable, .resizable],
backing: .buffered,
defer: false
)
window.titlebarAppearsTransparent = false
window.titleVisibility = .visible
window.center()
window.title = "Enhanced Terminal"
window.isReleasedWhenClosed = false
self.init(window: window)
setupUI()
setupPanels()
setupNotifications()
// Restore previous session or create new tab
restoreSession()
// Force window to show
window.makeKeyAndOrderFront(nil)
window.orderFrontRegardless()
// Debug: Print tab bar info
NSLog("Tab bar frame: \(tabBar.frame)")
NSLog("Tab bar subviews: \(tabBar.arrangedSubviews.count)")
NSLog("Window content view: \(window.contentView?.frame ?? .zero)")
}
private func restoreSession() {
let savedSessions = tabManager.loadSession()
if savedSessions.isEmpty {
// No saved session, create default tab
addNewTab()
} else {
// Restore saved tabs
for sessionData in savedSessions {
let session = tabManager.addTab(title: sessionData.title)
session.title = sessionData.title // Override the auto-generated title
session.setupTextView(frame: contentView.bounds)
// Restore working directory if available
if let workingDir = sessionData.workingDirectory {
session.currentWorkingDirectory = workingDir
}
session.startShell()
// Change to saved directory
if let workingDir = sessionData.workingDirectory, workingDir != "~" {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
session.executeCommand("cd \(workingDir)")
}
}
}
// Restore current tab index
let savedIndex = tabManager.getSavedCurrentIndex()
updateTabBar()
switchToTab(index: min(savedIndex, tabManager.tabs.count - 1))
}
}
private func setupUI() {
guard let window = window else { return }
let mainContainer = NSView(frame: window.contentView!.bounds)
mainContainer.autoresizingMask = [.width, .height]
// Tab bar - positioned below the title bar
let tabBarHeight: CGFloat = 28
let topMargin: CGFloat = 10 // Space below title bar
tabBar = NSStackView(frame: NSRect(x: 0, y: mainContainer.bounds.height - tabBarHeight - topMargin, width: mainContainer.bounds.width, height: tabBarHeight))
tabBar.orientation = .horizontal
tabBar.distribution = .fill
tabBar.alignment = .centerY
tabBar.spacing = 5
tabBar.autoresizingMask = [.width, .minYMargin]
tabBar.wantsLayer = true
tabBar.layer?.backgroundColor = NSColor(white: 0.15, alpha: 1.0).cgColor // Dark gray background
tabBar.edgeInsets = NSEdgeInsets(top: 2, left: 10, bottom: 2, right: 10)
NSLog("Creating tab bar at: \(tabBar.frame)")
// Content view below tab bar
contentView = NSView(frame: NSRect(x: 0, y: 0, width: mainContainer.bounds.width, height: mainContainer.bounds.height - tabBarHeight - topMargin))
contentView.autoresizingMask = [.width, .height]
contentView.wantsLayer = true
contentView.layer?.backgroundColor = NSColor.black.cgColor
NSLog("Creating content view at: \(contentView.frame)")
mainContainer.addSubview(contentView)
mainContainer.addSubview(tabBar)
window.contentView = mainContainer
NSLog("Main container bounds: \(mainContainer.bounds)")
setupMenuBar()
}
private func setupPanels() {
historyPanel = HistorySearchPanel()
snippetPanel = SnippetPanel()
aiPanel = AIGeneratorPanel()
settingsWindow = SettingsWindowController()
}
private func setupNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(themeChanged), name: NSNotification.Name("ThemeChanged"), object: nil)
}
@objc private func themeChanged() {
tabManager.tabs.forEach { $0.applyTheme() }
}
private func setupMenuBar() {
let mainMenu = NSMenu()
// App menu
let appMenuItem = NSMenuItem()
mainMenu.addItem(appMenuItem)
let appMenu = NSMenu()
appMenuItem.submenu = appMenu
appMenu.addItem(NSMenuItem(title: "About Enhanced Terminal", action: #selector(showAbout), keyEquivalent: ""))
appMenu.addItem(NSMenuItem.separator())
appMenu.addItem(NSMenuItem(title: "Settings...", action: #selector(openSettings), keyEquivalent: ","))
appMenu.addItem(NSMenuItem.separator())
appMenu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
// File menu
let fileMenuItem = NSMenuItem(title: "File", action: nil, keyEquivalent: "")
mainMenu.addItem(fileMenuItem)
let fileMenu = NSMenu(title: "File")
fileMenuItem.submenu = fileMenu
fileMenu.addItem(NSMenuItem(title: "New Tab", action: #selector(addNewTab), keyEquivalent: "t"))
fileMenu.addItem(NSMenuItem(title: "Close Tab", action: #selector(closeCurrentTab), keyEquivalent: "w"))
fileMenu.addItem(NSMenuItem.separator())
fileMenu.addItem(NSMenuItem(title: "Split Pane Vertically", action: #selector(splitPaneVertically), keyEquivalent: "d"))
fileMenu.addItem(NSMenuItem(title: "Split Pane Horizontally", action: #selector(splitPaneHorizontally), keyEquivalent: "D"))
let closePaneItem = NSMenuItem(title: "Close Pane", action: #selector(closePane), keyEquivalent: "w")
closePaneItem.keyEquivalentModifierMask = [.command, .shift]
fileMenu.addItem(closePaneItem)
fileMenu.addItem(NSMenuItem.separator())
fileMenu.addItem(NSMenuItem(title: "Clear Screen", action: #selector(clearScreen), keyEquivalent: "k"))
fileMenu.addItem(NSMenuItem(title: "Clear Scrollback", action: #selector(clearScrollback), keyEquivalent: "K"))
// Edit menu
let editMenuItem = NSMenuItem(title: "Edit", action: nil, keyEquivalent: "")
mainMenu.addItem(editMenuItem)
let editMenu = NSMenu(title: "Edit")
editMenuItem.submenu = editMenu
editMenu.addItem(NSMenuItem(title: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c"))
editMenu.addItem(NSMenuItem(title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v"))
editMenu.addItem(NSMenuItem(title: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a"))
editMenu.addItem(NSMenuItem.separator())
editMenu.addItem(NSMenuItem(title: "Find...", action: #selector(showFindPanel), keyEquivalent: "f"))
editMenu.addItem(NSMenuItem.separator())
let searchHistoryItem = NSMenuItem(title: "Search History", action: #selector(searchHistory), keyEquivalent: "r")
searchHistoryItem.keyEquivalentModifierMask = [.command]
editMenu.addItem(searchHistoryItem)
// View menu
let viewMenuItem = NSMenuItem(title: "View", action: nil, keyEquivalent: "")
mainMenu.addItem(viewMenuItem)
let viewMenu = NSMenu(title: "View")
viewMenuItem.submenu = viewMenu
viewMenu.addItem(NSMenuItem(title: "Toggle Fullscreen", action: #selector(NSWindow.toggleFullScreen(_:)), keyEquivalent: "f"))
viewMenu.addItem(NSMenuItem.separator())
viewMenu.addItem(NSMenuItem(title: "Increase Font Size", action: #selector(increaseFontSize), keyEquivalent: "+"))
viewMenu.addItem(NSMenuItem(title: "Decrease Font Size", action: #selector(decreaseFontSize), keyEquivalent: "-"))
viewMenu.addItem(NSMenuItem(title: "Reset Font Size", action: #selector(resetFontSize), keyEquivalent: "0"))
// Tools menu
let toolsMenuItem = NSMenuItem(title: "Tools", action: nil, keyEquivalent: "")
mainMenu.addItem(toolsMenuItem)
let toolsMenu = NSMenu(title: "Tools")
toolsMenuItem.submenu = toolsMenu
toolsMenu.addItem(NSMenuItem(title: "AI Command Generator", action: #selector(showAIGenerator), keyEquivalent: "k"))
toolsMenu.addItem(NSMenuItem(title: "Command Snippets", action: #selector(showSnippets), keyEquivalent: "p"))
toolsMenu.addItem(NSMenuItem.separator())
toolsMenu.addItem(NSMenuItem(title: "Keyboard Shortcuts", action: #selector(showShortcuts), keyEquivalent: "?"))
toolsMenu.addItem(NSMenuItem.separator())
toolsMenu.addItem(NSMenuItem(title: "Clear History", action: #selector(clearHistory), keyEquivalent: ""))
// Window menu
let windowMenuItem = NSMenuItem(title: "Window", action: nil, keyEquivalent: "")
mainMenu.addItem(windowMenuItem)
let windowMenu = NSMenu(title: "Window")
windowMenuItem.submenu = windowMenu
windowMenu.addItem(NSMenuItem(title: "Minimize", action: #selector(NSWindow.miniaturize(_:)), keyEquivalent: "m"))
windowMenu.addItem(NSMenuItem(title: "Zoom", action: #selector(NSWindow.zoom(_:)), keyEquivalent: ""))
NSApp.mainMenu = mainMenu
}
@objc func addNewTab() {
let session = tabManager.addTab()
session.setupTextView(frame: contentView.bounds)
session.startShell()
updateTabBar()
switchToTab(index: tabManager.tabs.count - 1)
}
@objc func closeCurrentTab() {
guard tabManager.tabs.count > 1 else {
// If it's the last tab, quit the app
NSApp.terminate(nil)
return
}
let alert = NSAlert()
alert.messageText = "Close Tab?"
alert.informativeText = "Are you sure you want to close this terminal session?"
alert.addButton(withTitle: "Close")
alert.addButton(withTitle: "Cancel")
alert.alertStyle = .warning
if alert.runModal() == .alertFirstButtonReturn {
tabManager.removeTab(at: tabManager.currentIndex)
updateTabBar()
switchToTab(index: min(tabManager.currentIndex, tabManager.tabs.count - 1))
}
}
private func updateTabBar() {
// Remove all existing views
tabBar.arrangedSubviews.forEach { $0.removeFromSuperview() }
// Add tab buttons
for (index, session) in tabManager.tabs.enumerated() {
// Create container for tab button and close button
let tabContainer = NSView()
tabContainer.wantsLayer = true
tabContainer.translatesAutoresizingMaskIntoConstraints = false
// Tab button
let tabButton = NSButton()
tabButton.title = session.title
tabButton.bezelStyle = .rounded
tabButton.tag = index
tabButton.target = self
tabButton.action = #selector(tabClicked(_:))
tabButton.font = NSFont.systemFont(ofSize: 11)
tabButton.translatesAutoresizingMaskIntoConstraints = false
// Add double-click for rename
let doubleClickGesture = NSClickGestureRecognizer(target: self, action: #selector(tabDoubleClicked(_:)))
doubleClickGesture.numberOfClicksRequired = 2
tabButton.addGestureRecognizer(doubleClickGesture)
// Highlight current tab
if index == tabManager.currentIndex {
tabButton.contentTintColor = .systemBlue
tabButton.isBordered = true
} else {
tabButton.contentTintColor = .systemGray
tabButton.isBordered = false
}
// Close button (X)
let closeButton = NSButton()
closeButton.title = "×"
closeButton.bezelStyle = .rounded
closeButton.tag = index
closeButton.target = self
closeButton.action = #selector(closeTabAtIndex(_:))
closeButton.font = NSFont.systemFont(ofSize: 13, weight: .bold)
closeButton.translatesAutoresizingMaskIntoConstraints = false
tabContainer.addSubview(tabButton)
tabContainer.addSubview(closeButton)
// Layout constraints
NSLayoutConstraint.activate([
tabButton.leadingAnchor.constraint(equalTo: tabContainer.leadingAnchor),
tabButton.centerYAnchor.constraint(equalTo: tabContainer.centerYAnchor),
tabButton.widthAnchor.constraint(equalToConstant: 60),
tabButton.heightAnchor.constraint(equalToConstant: 20),
closeButton.leadingAnchor.constraint(equalTo: tabButton.trailingAnchor, constant: 2),
closeButton.centerYAnchor.constraint(equalTo: tabContainer.centerYAnchor),
closeButton.widthAnchor.constraint(equalToConstant: 18),
closeButton.heightAnchor.constraint(equalToConstant: 20),
closeButton.trailingAnchor.constraint(equalTo: tabContainer.trailingAnchor),
tabContainer.heightAnchor.constraint(equalToConstant: 20)
])
// Prevent container from stretching
tabContainer.setContentHuggingPriority(.init(1000), for: .horizontal)
tabContainer.setContentCompressionResistancePriority(.init(1000), for: .horizontal)
tabBar.addArrangedSubview(tabContainer)
}
// Add new tab button
let newTabButton = NSButton()
newTabButton.title = "+"
newTabButton.bezelStyle = .rounded
newTabButton.target = self
newTabButton.action = #selector(addNewTab)
newTabButton.font = NSFont.systemFont(ofSize: 13)
newTabButton.translatesAutoresizingMaskIntoConstraints = false
newTabButton.setContentHuggingPriority(.init(1000), for: .horizontal)
NSLayoutConstraint.activate([
newTabButton.widthAnchor.constraint(equalToConstant: 22),
newTabButton.heightAnchor.constraint(equalToConstant: 20)
])
tabBar.addArrangedSubview(newTabButton)
// Add spacer at the end to push everything to the left
let spacer = NSView()
spacer.setContentHuggingPriority(.init(1), for: .horizontal)
tabBar.addArrangedSubview(spacer)
// Force layout update
tabBar.needsLayout = true
tabBar.layoutSubtreeIfNeeded()
}
@objc func closeTabAtIndex(_ sender: NSButton) {
let index = sender.tag
// Don't close if it's the last tab
guard tabManager.tabs.count > 1 else {
NSApp.terminate(nil)
return
}
// Confirm if there's content
let alert = NSAlert()
alert.messageText = "Close Tab?"
alert.informativeText = "Are you sure you want to close this tab?"
alert.addButton(withTitle: "Close")
alert.addButton(withTitle: "Cancel")
alert.alertStyle = .warning
if alert.runModal() == .alertFirstButtonReturn {
tabManager.removeTab(at: index)
// Switch to previous tab or first tab
let newIndex = min(index, tabManager.tabs.count - 1)
switchToTab(index: newIndex)
}
}
@objc func tabClicked(_ sender: NSButton) {
switchToTab(index: sender.tag)
}
@objc func tabDoubleClicked(_ sender: NSClickGestureRecognizer) {
guard let button = sender.view as? NSButton else { return }
let index = button.tag
let alert = NSAlert()
alert.messageText = "Rename Tab"
alert.informativeText = "Enter a new name for this tab:"
let textField = NSTextField(frame: NSRect(x: 0, y: 0, width: 200, height: 24))
textField.stringValue = tabManager.tabs[index].title
textField.placeholderString = "Tab name"
alert.accessoryView = textField
alert.addButton(withTitle: "Rename")
alert.addButton(withTitle: "Cancel")
alert.window.initialFirstResponder = textField
if alert.runModal() == .alertFirstButtonReturn {
let newName = textField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
if !newName.isEmpty {
tabManager.tabs[index].title = newName
updateTabBar()
tabManager.saveSession()
}
}
}
private func switchToTab(index: Int) {
guard index < tabManager.tabs.count else { return }
// Remove current view from content view (but don't destroy it)
contentView.subviews.forEach { $0.removeFromSuperview() }
// Select the new tab
tabManager.selectTab(at: index)
tabManager.saveSession()
// Add the selected tab's scroll view to content view
if let session = tabManager.getCurrentSession() {
session.scrollView.frame = contentView.bounds
session.scrollView.autoresizingMask = [.width, .height]
contentView.addSubview(session.scrollView)
// Make the text view first responder
window?.makeFirstResponder(session.textView)
}
updateTabBar()
}
@objc func searchHistory() {
guard let window = window else { return }
historyPanel.show(relativeTo: window) { [weak self] command in
if let command = command, let session = self?.tabManager.getCurrentSession() {
session.insertCommand(command)
}
}
}
@objc func showSnippets() {
guard let window = window else { return }
snippetPanel.show(relativeTo: window) { [weak self] snippet in
if let snippet = snippet, let session = self?.tabManager.getCurrentSession() {
session.insertCommand(snippet.command)
}
}
}
@objc func showAIGenerator() {
guard AIManager.shared.isAvailable() else {
let alert = NSAlert()
alert.messageText = "AI Not Configured"
alert.informativeText = "Please configure an AI provider in Settings (Cmd+,) first.\n\nYou can use:\n• OpenAI (requires API key)\n• Ollama (free, local)"
alert.alertStyle = .informational
alert.addButton(withTitle: "Open Settings")
alert.addButton(withTitle: "Cancel")
if alert.runModal() == .alertFirstButtonReturn {
openSettings()
}
return
}
guard let window = window else { return }
aiPanel.show(relativeTo: window) { [weak self] command in
if let command = command, let session = self?.tabManager.getCurrentSession() {
session.insertCommand(command)
}
}
}
@objc func openSettings() {
settingsWindow.showWindow(nil)
}
@objc func clearScreen() {
if let session = tabManager.getCurrentSession() {
session.textView.string = ""
session.showPrompt()
}
}
@objc func clearScrollback() {
if let session = tabManager.getCurrentSession() {
let alert = NSAlert()
alert.messageText = "Clear Scrollback?"
alert.informativeText = "This will clear all terminal output history."
alert.addButton(withTitle: "Clear")
alert.addButton(withTitle: "Cancel")
alert.alertStyle = .warning
if alert.runModal() == .alertFirstButtonReturn {
session.textView.string = ""
session.showPrompt()
}
}
}
@objc func showFindPanel() {
if let session = tabManager.getCurrentSession() {
session.textView.performTextFinderAction(nil)
}
}
@objc func showShortcuts() {
let alert = NSAlert()
alert.messageText = "Keyboard Shortcuts"
alert.informativeText = """
File:
Cmd+T - New Tab
Cmd+W - Close Tab
Cmd+D - Split Pane Vertically
Cmd+Shift+D - Split Pane Horizontally
Cmd+Shift+W - Close Pane
Cmd+K - Clear Screen
Cmd+Shift+K - Clear Scrollback
Edit:
Cmd+C - Copy
Cmd+V - Paste
Cmd+A - Select All
Cmd+F - Find in Terminal
Cmd+R - Search History
Tools:
Cmd+K - AI Command Generator
Cmd+P - Command Snippets
Cmd+, - Settings
Cmd+? - This Help
View:
Cmd+Plus - Increase Font
Cmd+Minus - Decrease Font
Cmd+0 - Reset Font
Cmd+F - Toggle Fullscreen
Terminal:
Tab - Accept Auto-suggestion
Double-click Tab - Rename Tab
"""
alert.alertStyle = .informational
alert.addButton(withTitle: "OK")
alert.runModal()
}
@objc func splitPaneVertically() {
guard let currentSession = tabManager.getCurrentSession() else { return }
// If split view exists with wrong orientation, recreate it
if let existing = splitView, !existing.isVertical {
// Remove and recreate with correct orientation
existing.removeFromSuperview()
splitView = nil
panes = []
}
// Create split view if it doesn't exist
if splitView == nil {
splitView = NSSplitView(frame: contentView.bounds)
splitView?.isVertical = true // Side-by-side (vertical divider)
splitView?.dividerStyle = .thin
splitView?.autoresizingMask = [.width, .height]
// Move current session to split view
currentSession.scrollView.removeFromSuperview()
splitView?.addArrangedSubview(currentSession.scrollView)
panes = [currentSession]
}
// Create new pane
let newSession = TerminalSession(title: "Pane \(panes.count + 1)")
newSession.setupTextView(frame: CGRect(x: 0, y: 0, width: contentView.bounds.width / 2, height: contentView.bounds.height))
newSession.startShell()
splitView?.addArrangedSubview(newSession.scrollView)
panes.append(newSession)
activePaneIndex = panes.count - 1
// Add split view to content if not already there
if splitView?.superview == nil {
contentView.subviews.forEach { $0.removeFromSuperview() }
contentView.addSubview(splitView!)
}
newSession.textView.window?.makeFirstResponder(newSession.textView)
}
@objc func splitPaneHorizontally() {
guard let currentSession = tabManager.getCurrentSession() else { return }
// If split view exists with wrong orientation, recreate it
if let existing = splitView, existing.isVertical {
// Remove and recreate with correct orientation
existing.removeFromSuperview()
splitView = nil
panes = []
}
// Create split view if it doesn't exist
if splitView == nil {
splitView = NSSplitView(frame: contentView.bounds)
splitView?.isVertical = false // Top-bottom (horizontal divider)
splitView?.dividerStyle = .thin
splitView?.autoresizingMask = [.width, .height]
// Move current session to split view
currentSession.scrollView.removeFromSuperview()
splitView?.addArrangedSubview(currentSession.scrollView)
panes = [currentSession]
}
// Create new pane
let newSession = TerminalSession(title: "Pane \(panes.count + 1)")
newSession.setupTextView(frame: CGRect(x: 0, y: 0, width: contentView.bounds.width, height: contentView.bounds.height / 2))
newSession.startShell()
splitView?.addArrangedSubview(newSession.scrollView)
panes.append(newSession)
activePaneIndex = panes.count - 1
// Add split view to content if not already there
if splitView?.superview == nil {
contentView.subviews.forEach { $0.removeFromSuperview() }
contentView.addSubview(splitView!)
}
newSession.textView.window?.makeFirstResponder(newSession.textView)
}
@objc func closePane() {
// Only close panes if we have splits
guard panes.count > 1 else {
// No splits, just close the tab normally
closeCurrentTab()
return
}
// Find which pane has focus
var paneToClose = activePaneIndex
// If we can't determine active pane, close the last one
if paneToClose >= panes.count {
paneToClose = panes.count - 1
}
let pane = panes[paneToClose]
pane.cleanup()
pane.scrollView.removeFromSuperview()
panes.remove(at: paneToClose)
if panes.count == 1 {
// Only one pane left, remove split view
splitView?.removeFromSuperview()
splitView = nil
let remainingSession = panes[0]
remainingSession.scrollView.removeFromSuperview()
remainingSession.scrollView.frame = contentView.bounds
contentView.addSubview(remainingSession.scrollView)
remainingSession.textView.window?.makeFirstResponder(remainingSession.textView)
panes = []
} else {
// Still have multiple panes
activePaneIndex = min(paneToClose, panes.count - 1)
if activePaneIndex < panes.count {
panes[activePaneIndex].textView.window?.makeFirstResponder(panes[activePaneIndex].textView)
}
}
}
@objc func clearHistory() {
let alert = NSAlert()
alert.messageText = "Clear Command History?"
alert.informativeText = "This will permanently delete all command history."
alert.addButton(withTitle: "Clear")
alert.addButton(withTitle: "Cancel")
alert.alertStyle = .warning
if alert.runModal() == .alertFirstButtonReturn {
HistoryManager.shared.clearHistory()
}
}
@objc func increaseFontSize() {
TerminalSettings.shared.fontSize += 1
tabManager.tabs.forEach { $0.applyTheme() }
}
@objc func decreaseFontSize() {
TerminalSettings.shared.fontSize = max(8, TerminalSettings.shared.fontSize - 1)
tabManager.tabs.forEach { $0.applyTheme() }
}
@objc func resetFontSize() {
TerminalSettings.shared.fontSize = 14
tabManager.tabs.forEach { $0.applyTheme() }
}
@objc func showAbout() {
let alert = NSAlert()
alert.messageText = "Enhanced Terminal v3.0"
alert.informativeText = """
A feature-rich macOS terminal emulator
Features:
• Multiple tabs
• Command history search (Cmd+R)
• Auto-suggestions
• AI command generation
• Command snippets
• Multiple themes
• Customizable settings
Built with Swift and Cocoa
"""
alert.alertStyle = .informational
alert.addButton(withTitle: "OK")
alert.runModal()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}