import Cocoa // MARK: - History Search Panel class HistorySearchPanel: NSPanel { private var tableView: NSTableView! private var searchField: NSTextField! private var filteredHistory: [String] = [] private var completion: ((String?) -> Void)? init() { super.init( contentRect: NSRect(x: 0, y: 0, width: 500, height: 400), styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: false ) title = "Command History" setupUI() } private func setupUI() { let container = NSView(frame: contentView!.bounds) container.autoresizingMask = [.width, .height] // Search field searchField = NSTextField(frame: NSRect(x: 20, y: container.bounds.height - 50, width: container.bounds.width - 40, height: 30)) searchField.placeholderString = "Search history..." searchField.target = self searchField.action = #selector(searchChanged) searchField.autoresizingMask = [.width, .minYMargin] // Table view let scrollView = NSScrollView(frame: NSRect(x: 20, y: 20, width: container.bounds.width - 40, height: container.bounds.height - 90)) scrollView.autoresizingMask = [.width, .height] scrollView.hasVerticalScroller = true tableView = NSTableView(frame: scrollView.bounds) tableView.headerView = nil let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("command")) column.width = scrollView.bounds.width tableView.addTableColumn(column) tableView.dataSource = self tableView.delegate = self tableView.target = self tableView.doubleAction = #selector(selectCommand) scrollView.documentView = tableView container.addSubview(searchField) container.addSubview(scrollView) contentView = container filteredHistory = HistoryManager.shared.getRecentCommands(50) tableView.reloadData() } @objc private func searchChanged() { let query = searchField.stringValue filteredHistory = HistoryManager.shared.searchHistory(query) tableView.reloadData() } @objc private func selectCommand() { let row = tableView.selectedRow guard row >= 0 && row < filteredHistory.count else { return } completion?(filteredHistory[row]) close() } func show(relativeTo window: NSWindow, completion: @escaping (String?) -> Void) { self.completion = completion center() makeKeyAndOrderFront(nil) } } extension HistorySearchPanel: NSTableViewDataSource, NSTableViewDelegate { func numberOfRows(in tableView: NSTableView) -> Int { return filteredHistory.count } func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { let text = NSTextField(labelWithString: filteredHistory[row]) text.font = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular) return text } } // MARK: - Snippet Panel class SnippetPanel: NSPanel { private var tableView: NSTableView! private var searchField: NSTextField! private var filteredSnippets: [SnippetManager.Snippet] = [] private var completion: ((SnippetManager.Snippet?) -> Void)? init() { super.init( contentRect: NSRect(x: 0, y: 0, width: 600, height: 500), styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: false ) title = "Command Snippets" setupUI() } private func setupUI() { let container = NSView(frame: contentView!.bounds) container.autoresizingMask = [.width, .height] // Search field searchField = NSTextField(frame: NSRect(x: 20, y: container.bounds.height - 50, width: container.bounds.width - 140, height: 30)) searchField.placeholderString = "Search snippets..." searchField.target = self searchField.action = #selector(searchChanged) searchField.autoresizingMask = [.width, .minYMargin] // Add button let addButton = NSButton(frame: NSRect(x: container.bounds.width - 110, y: container.bounds.height - 50, width: 90, height: 30)) addButton.title = "Add New" addButton.bezelStyle = .rounded addButton.target = self addButton.action = #selector(addSnippet) addButton.autoresizingMask = [.minXMargin, .minYMargin] // Table view let scrollView = NSScrollView(frame: NSRect(x: 20, y: 20, width: container.bounds.width - 40, height: container.bounds.height - 90)) scrollView.autoresizingMask = [.width, .height] scrollView.hasVerticalScroller = true tableView = NSTableView(frame: scrollView.bounds) let nameColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("name")) nameColumn.title = "Name" nameColumn.width = 150 let commandColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("command")) commandColumn.title = "Command" commandColumn.width = 300 let categoryColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("category")) categoryColumn.title = "Category" categoryColumn.width = 100 tableView.addTableColumn(nameColumn) tableView.addTableColumn(commandColumn) tableView.addTableColumn(categoryColumn) tableView.dataSource = self tableView.delegate = self tableView.target = self tableView.doubleAction = #selector(selectSnippet) scrollView.documentView = tableView container.addSubview(searchField) container.addSubview(addButton) container.addSubview(scrollView) contentView = container filteredSnippets = SnippetManager.shared.getAllSnippets() tableView.reloadData() } @objc private func searchChanged() { let query = searchField.stringValue filteredSnippets = SnippetManager.shared.searchSnippets(query) tableView.reloadData() } @objc private func addSnippet() { let alert = NSAlert() alert.messageText = "Add New Snippet" alert.informativeText = "Enter snippet details:" let nameField = NSTextField(frame: NSRect(x: 0, y: 60, width: 300, height: 24)) nameField.placeholderString = "Name" let commandField = NSTextField(frame: NSRect(x: 0, y: 30, width: 300, height: 24)) commandField.placeholderString = "Command" let categoryField = NSTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 24)) categoryField.placeholderString = "Category" let view = NSView(frame: NSRect(x: 0, y: 0, width: 300, height: 90)) view.addSubview(nameField) view.addSubview(commandField) view.addSubview(categoryField) alert.accessoryView = view alert.addButton(withTitle: "Add") alert.addButton(withTitle: "Cancel") if alert.runModal() == .alertFirstButtonReturn { let snippet = SnippetManager.Snippet( name: nameField.stringValue, command: commandField.stringValue, category: categoryField.stringValue.isEmpty ? "General" : categoryField.stringValue ) SnippetManager.shared.addSnippet(snippet) filteredSnippets = SnippetManager.shared.getAllSnippets() tableView.reloadData() } } @objc private func selectSnippet() { let row = tableView.selectedRow guard row >= 0 && row < filteredSnippets.count else { return } completion?(filteredSnippets[row]) close() } func show(relativeTo window: NSWindow, completion: @escaping (SnippetManager.Snippet?) -> Void) { self.completion = completion center() makeKeyAndOrderFront(nil) } } extension SnippetPanel: NSTableViewDataSource, NSTableViewDelegate { func numberOfRows(in tableView: NSTableView) -> Int { return filteredSnippets.count } func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { let snippet = filteredSnippets[row] let identifier = tableColumn?.identifier.rawValue ?? "" let text: String switch identifier { case "name": text = snippet.name case "command": text = snippet.command case "category": text = snippet.category default: text = "" } let textField = NSTextField(labelWithString: text) textField.font = NSFont.systemFont(ofSize: 12) return textField } } // MARK: - AI Generator Panel class AIGeneratorPanel: NSPanel { private var promptField: NSTextField! private var resultField: NSTextView! private var generateButton: NSButton! private var completion: ((String?) -> Void)? init() { super.init( contentRect: NSRect(x: 0, y: 0, width: 500, height: 300), styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: false ) title = "AI Command Generator" setupUI() } private func setupUI() { let container = NSView(frame: contentView!.bounds) container.autoresizingMask = [.width, .height] // Prompt field let promptLabel = NSTextField(labelWithString: "Describe what you want to do:") promptLabel.frame = NSRect(x: 20, y: container.bounds.height - 40, width: 200, height: 20) promptLabel.autoresizingMask = [.minYMargin] promptField = NSTextField(frame: NSRect(x: 20, y: container.bounds.height - 70, width: container.bounds.width - 40, height: 24)) promptField.placeholderString = "e.g., find all PDF files larger than 10MB" promptField.autoresizingMask = [.width, .minYMargin] // Generate button generateButton = NSButton(frame: NSRect(x: 20, y: container.bounds.height - 110, width: 100, height: 30)) generateButton.title = "Generate" generateButton.bezelStyle = .rounded generateButton.target = self generateButton.action = #selector(generate) generateButton.autoresizingMask = [.minYMargin] // Result field let scrollView = NSScrollView(frame: NSRect(x: 20, y: 20, width: container.bounds.width - 40, height: container.bounds.height - 160)) scrollView.autoresizingMask = [.width, .height] scrollView.hasVerticalScroller = true resultField = NSTextView(frame: scrollView.bounds) resultField.isEditable = false resultField.font = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular) resultField.string = "Generated command will appear here..." scrollView.documentView = resultField // Use button let useButton = NSButton(frame: NSRect(x: container.bounds.width - 120, y: container.bounds.height - 110, width: 100, height: 30)) useButton.title = "Use Command" useButton.bezelStyle = .rounded useButton.target = self useButton.action = #selector(useCommand) useButton.autoresizingMask = [.minXMargin, .minYMargin] container.addSubview(promptLabel) container.addSubview(promptField) container.addSubview(generateButton) container.addSubview(scrollView) container.addSubview(useButton) contentView = container } @objc private func generate() { let prompt = promptField.stringValue guard !prompt.isEmpty else { return } generateButton.isEnabled = false resultField.string = "Generating..." AIManager.shared.generateCommand(from: prompt) { [weak self] result in DispatchQueue.main.async { self?.generateButton.isEnabled = true switch result { case .success(let command): self?.resultField.string = command case .failure(let error): self?.resultField.string = "Error: \(error.localizedDescription)" } } } } @objc private func useCommand() { let command = resultField.string guard !command.isEmpty && !command.hasPrefix("Error:") && !command.hasPrefix("Generating") else { return } completion?(command) close() } func show(relativeTo window: NSWindow, completion: @escaping (String?) -> Void) { self.completion = completion center() makeKeyAndOrderFront(nil) } } // MARK: - Settings Window class SettingsWindowController: NSWindowController { convenience init() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 500, height: 400), styleMask: [.titled, .closable], backing: .buffered, defer: false ) window.title = "Settings" window.center() self.init(window: window) setupUI() } private func setupUI() { guard let window = window else { return } let container = NSView(frame: window.contentView!.bounds) container.autoresizingMask = [.width, .height] var yPos = container.bounds.height - 40 // Theme selection let themeLabel = NSTextField(labelWithString: "Theme:") themeLabel.frame = NSRect(x: 20, y: yPos, width: 100, height: 20) let themePopup = NSPopUpButton(frame: NSRect(x: 130, y: yPos - 5, width: 200, height: 30)) themePopup.addItems(withTitles: TerminalSettings.Theme.allThemes.map { $0.name }) themePopup.target = self themePopup.action = #selector(themeChanged(_:)) container.addSubview(themeLabel) container.addSubview(themePopup) yPos -= 50 // AI Provider let aiLabel = NSTextField(labelWithString: "AI Provider:") aiLabel.frame = NSRect(x: 20, y: yPos, width: 100, height: 20) let aiPopup = NSPopUpButton(frame: NSRect(x: 130, y: yPos - 5, width: 200, height: 30)) aiPopup.addItems(withTitles: ["None", "OpenAI", "Ollama"]) aiPopup.target = self aiPopup.action = #selector(aiProviderChanged(_:)) container.addSubview(aiLabel) container.addSubview(aiPopup) yPos -= 50 // OpenAI Key let openAILabel = NSTextField(labelWithString: "OpenAI Key:") openAILabel.frame = NSRect(x: 20, y: yPos, width: 100, height: 20) let openAIField = NSSecureTextField(frame: NSRect(x: 130, y: yPos, width: 300, height: 24)) openAIField.placeholderString = "sk-..." openAIField.stringValue = TerminalSettings.shared.openAIKey openAIField.target = self openAIField.action = #selector(openAIKeyChanged(_:)) container.addSubview(openAILabel) container.addSubview(openAIField) yPos -= 50 // Ollama URL let ollamaLabel = NSTextField(labelWithString: "Ollama URL:") ollamaLabel.frame = NSRect(x: 20, y: yPos, width: 100, height: 20) let ollamaField = NSTextField(frame: NSRect(x: 130, y: yPos, width: 300, height: 24)) ollamaField.placeholderString = "http://localhost:11434" ollamaField.stringValue = TerminalSettings.shared.ollamaURL ollamaField.target = self ollamaField.action = #selector(ollamaURLChanged(_:)) container.addSubview(ollamaLabel) container.addSubview(ollamaField) yPos -= 50 // Ollama Model let modelLabel = NSTextField(labelWithString: "Ollama Model:") modelLabel.frame = NSRect(x: 20, y: yPos, width: 100, height: 20) let modelField = NSTextField(frame: NSRect(x: 130, y: yPos, width: 300, height: 24)) modelField.placeholderString = "llama2" modelField.stringValue = TerminalSettings.shared.ollamaModel modelField.target = self modelField.action = #selector(ollamaModelChanged(_:)) container.addSubview(modelLabel) container.addSubview(modelField) window.contentView = container } @objc private func themeChanged(_ sender: NSPopUpButton) { let themes = TerminalSettings.Theme.allThemes if sender.indexOfSelectedItem < themes.count { TerminalSettings.shared.theme = themes[sender.indexOfSelectedItem] } } @objc private func aiProviderChanged(_ sender: NSPopUpButton) { let providers: [TerminalSettings.AIProvider] = [.none, .openai, .ollama] if sender.indexOfSelectedItem < providers.count { TerminalSettings.shared.aiProvider = providers[sender.indexOfSelectedItem] AIManager.shared.updateService() } } @objc private func openAIKeyChanged(_ sender: NSTextField) { TerminalSettings.shared.openAIKey = sender.stringValue AIManager.shared.updateService() } @objc private func ollamaURLChanged(_ sender: NSTextField) { TerminalSettings.shared.ollamaURL = sender.stringValue AIManager.shared.updateService() } @objc private func ollamaModelChanged(_ sender: NSTextField) { TerminalSettings.shared.ollamaModel = sender.stringValue AIManager.shared.updateService() } }