import Cocoa class MainWindowController: NSWindowController { var tabManager = TabManager() var tabBar: NSStackView! var contentView: NSView! 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) session.startShell() } // 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: "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: "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 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) } }