import Cocoa // MARK: - Hover Close Button class HoverCloseButton: NSButton { override func mouseEntered(with event: NSEvent) { super.mouseEntered(with: event) NSAnimationContext.runAnimationGroup { context in context.duration = 0.15 self.layer?.backgroundColor = NSColor(white: 0.3, alpha: 0.85).cgColor self.contentTintColor = NSColor.white } } override func mouseExited(with event: NSEvent) { super.mouseExited(with: event) NSAnimationContext.runAnimationGroup { context in context.duration = 0.15 self.layer?.backgroundColor = NSColor(white: 0.2, alpha: 0.6).cgColor self.contentTintColor = NSColor(white: 0.8, alpha: 0.9) } } override func updateTrackingAreas() { super.updateTrackingAreas() // Remove old tracking areas for trackingArea in trackingAreas { removeTrackingArea(trackingArea) } // Add new tracking area let trackingArea = NSTrackingArea( rect: bounds, options: [.mouseEnteredAndExited, .activeAlways, .inVisibleRect], owner: self, userInfo: nil ) addTrackingArea(trackingArea) } } 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! var zoomedPane: TerminalSession? var preZoomSplitView: NSSplitView? var lastTabClickTime: Date? var lastTabClickIndex: Int = -1 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) let zoomPaneItem = NSMenuItem(title: "Zoom Pane", action: #selector(toggleZoomPane), keyEquivalent: "\r") zoomPaneItem.keyEquivalentModifierMask = [.command, .shift] fileMenu.addItem(zoomPaneItem) 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: "i")) 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 // 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) { let index = sender.tag let now = Date() // Check for double-click (within 0.3 seconds) if let lastClick = lastTabClickTime, lastTabClickIndex == index, now.timeIntervalSince(lastClick) < 0.3 { // Double-click detected tabDoubleClicked(sender) lastTabClickTime = nil lastTabClickIndex = -1 } else { // Single click lastTabClickTime = now lastTabClickIndex = index switchToTab(index: index) } } @objc func tabDoubleClicked(_ sender: NSButton) { let index = sender.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 (side-by-side) Cmd+Shift+D - Split Pane Horizontally (top-bottom) Cmd+Shift+Enter - Zoom/Unzoom 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 Middle-click - Paste Tools: Cmd+I - 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 Up/Down - Navigate Command History Double-click Tab - Rename Tab Click × button - Close split pane Cmd+Click URL - Open in browser Select text - Auto-copy to clipboard """ 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] splitView?.delegate = self // Move current session to split view with close button currentSession.scrollView.removeFromSuperview() let containerView = addCloseButtonToPane(currentSession.scrollView, index: 0) splitView?.addArrangedSubview(containerView) 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() let newContainerView = addCloseButtonToPane(newSession.scrollView, index: panes.count) splitView?.addArrangedSubview(newContainerView) 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] splitView?.delegate = self // Move current session to split view with close button currentSession.scrollView.removeFromSuperview() let containerView = addCloseButtonToPane(currentSession.scrollView, index: 0) splitView?.addArrangedSubview(containerView) 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() let newContainerView = addCloseButtonToPane(newSession.scrollView, index: panes.count) splitView?.addArrangedSubview(newContainerView) 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) } private func addCloseButtonToPane(_ scrollView: NSScrollView, index: Int) -> NSView { // Create a container view that will hold both the scroll view and the close button let containerView = NSView(frame: scrollView.frame) containerView.autoresizingMask = [.width, .height] containerView.wantsLayer = true // Adjust scroll view frame to fill container scrollView.frame = containerView.bounds scrollView.autoresizingMask = [.width, .height] containerView.addSubview(scrollView) // Create close button with subtle styling let closeButton = HoverCloseButton(frame: NSRect(x: containerView.bounds.width - 35, y: containerView.bounds.height - 35, width: 24, height: 24)) closeButton.title = "×" closeButton.bezelStyle = .circular closeButton.font = NSFont.systemFont(ofSize: 16, weight: .medium) closeButton.tag = 9999 closeButton.target = self closeButton.action = #selector(closePaneFromButton(_:)) closeButton.toolTip = "Close pane (Cmd+Shift+W)" closeButton.autoresizingMask = [.minXMargin, .minYMargin] closeButton.wantsLayer = true closeButton.layer?.backgroundColor = NSColor(white: 0.2, alpha: 0.6).cgColor closeButton.layer?.cornerRadius = 12 closeButton.contentTintColor = NSColor(white: 0.8, alpha: 0.9) // Store the pane index objc_setAssociatedObject(closeButton, "paneIndex", index, .OBJC_ASSOCIATION_RETAIN) // Add button on top of scroll view containerView.addSubview(closeButton) return containerView } @objc func closePaneFromButton(_ sender: NSButton) { guard let index = objc_getAssociatedObject(sender, "paneIndex") as? Int else { return } // Find the actual current index (it might have changed) guard index < panes.count else { return } activePaneIndex = index closePane() } @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 toggleZoomPane() { if zoomedPane != nil { // Unzoom - restore split view unzoomPane() } else { // Zoom current pane zoomPane() } } private func zoomPane() { guard panes.count > 1 else { return } // Save current split view state preZoomSplitView = splitView zoomedPane = panes[activePaneIndex] // Remove split view splitView?.removeFromSuperview() // Show only the active pane if let zoomed = zoomedPane { zoomed.scrollView.removeFromSuperview() zoomed.scrollView.frame = contentView.bounds contentView.addSubview(zoomed.scrollView) zoomed.textView.window?.makeFirstResponder(zoomed.textView) } } private func unzoomPane() { guard let zoomed = zoomedPane, let savedSplit = preZoomSplitView else { return } // Remove zoomed pane zoomed.scrollView.removeFromSuperview() // Restore split view contentView.addSubview(savedSplit) savedSplit.frame = contentView.bounds // Clear zoom state zoomedPane = nil preZoomSplitView = nil // Focus the previously zoomed pane zoomed.textView.window?.makeFirstResponder(zoomed.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) } } // MARK: - NSSplitView Delegate extension MainWindowController: NSSplitViewDelegate { func splitView(_ splitView: NSSplitView, canCollapseSubview subview: NSView) -> Bool { return false } func splitView(_ splitView: NSSplitView, constrainMinCoordinate proposedMinimumPosition: CGFloat, ofSubviewAt dividerIndex: Int) -> CGFloat { return 100 // Minimum pane size } func splitView(_ splitView: NSSplitView, constrainMaxCoordinate proposedMaximumPosition: CGFloat, ofSubviewAt dividerIndex: Int) -> CGFloat { let totalSize = splitView.isVertical ? splitView.bounds.width : splitView.bounds.height return totalSize - 100 // Minimum pane size } }