import Cocoa // MARK: - Terminal Session class TerminalSession: NSObject, NSTextViewDelegate, NSMenuDelegate { var textView: NSTextView! var scrollView: NSScrollView! // Store scroll view as property var shellTask: Process? var inputPipe: Pipe? var outputPipe: Pipe? var promptPosition = 0 var sessionID: UUID var title: String private var suggestionLabel: NSTextField? private var currentSuggestion: String? init(title: String = "Terminal") { self.sessionID = UUID() self.title = title super.init() } func setupTextView(frame: NSRect) { scrollView = NSScrollView(frame: frame) scrollView.autoresizingMask = [.width, .height] scrollView.hasVerticalScroller = true scrollView.hasHorizontalScroller = false textView = NSTextView(frame: scrollView.bounds) textView.autoresizingMask = [.width, .height] textView.isEditable = true textView.delegate = self textView.isRichText = true textView.allowsUndo = true textView.usesFindBar = true applyTheme() scrollView.documentView = textView // Setup context menu setupContextMenu() // Setup suggestion label setupSuggestionLabel() } private func setupContextMenu() { let menu = NSMenu() menu.delegate = self let copyItem = NSMenuItem(title: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "") let pasteItem = NSMenuItem(title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "") let selectAllItem = NSMenuItem(title: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "") menu.addItem(copyItem) menu.addItem(pasteItem) menu.addItem(selectAllItem) menu.addItem(NSMenuItem.separator()) let saveSnippetItem = NSMenuItem(title: "Save as Snippet", action: #selector(saveSelectionAsSnippet), keyEquivalent: "") saveSnippetItem.target = self menu.addItem(saveSnippetItem) textView.menu = menu } @objc private func saveSelectionAsSnippet() { let selectedRange = textView.selectedRange() guard selectedRange.length > 0 else { return } let selectedText = (textView.string as NSString).substring(with: selectedRange).trimmingCharacters(in: .whitespacesAndNewlines) guard !selectedText.isEmpty else { return } let alert = NSAlert() alert.messageText = "Save as Snippet" alert.informativeText = "Enter details for this snippet:" let nameField = NSTextField(frame: NSRect(x: 0, y: 60, width: 300, height: 24)) nameField.placeholderString = "Snippet name" let categoryField = NSTextField(frame: NSRect(x: 0, y: 30, width: 300, height: 24)) categoryField.placeholderString = "Category (optional)" let descField = NSTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 24)) descField.placeholderString = "Description (optional)" let commandLabel = NSTextField(labelWithString: "Command: \(selectedText)") commandLabel.frame = NSRect(x: 0, y: 90, width: 300, height: 20) commandLabel.font = NSFont.systemFont(ofSize: 11) commandLabel.textColor = .secondaryLabelColor commandLabel.lineBreakMode = .byTruncatingTail let view = NSView(frame: NSRect(x: 0, y: 0, width: 300, height: 115)) view.addSubview(commandLabel) view.addSubview(nameField) view.addSubview(categoryField) view.addSubview(descField) alert.accessoryView = view alert.addButton(withTitle: "Save") alert.addButton(withTitle: "Cancel") if alert.runModal() == .alertFirstButtonReturn { let snippet = SnippetManager.Snippet( name: nameField.stringValue.isEmpty ? "Untitled" : nameField.stringValue, command: selectedText, description: descField.stringValue, category: categoryField.stringValue.isEmpty ? "General" : categoryField.stringValue ) SnippetManager.shared.addSnippet(snippet) // Show confirmation let confirmation = NSAlert() confirmation.messageText = "Snippet Saved" confirmation.informativeText = "Your snippet has been saved. Access it with Cmd+P." confirmation.alertStyle = .informational confirmation.addButton(withTitle: "OK") confirmation.runModal() } } private func setupSuggestionLabel() { suggestionLabel = NSTextField(frame: .zero) suggestionLabel?.isEditable = false suggestionLabel?.isSelectable = false suggestionLabel?.isBordered = false suggestionLabel?.backgroundColor = .clear suggestionLabel?.isHidden = true if let label = suggestionLabel { textView.addSubview(label) } } func applyTheme() { let theme = TerminalSettings.shared.theme textView?.backgroundColor = theme.background textView?.textColor = theme.foreground textView?.insertionPointColor = theme.cursor textView?.selectedTextAttributes = [ .backgroundColor: theme.selection ] textView?.font = NSFont(name: TerminalSettings.shared.fontName, size: TerminalSettings.shared.fontSize) ?? NSFont.monospacedSystemFont(ofSize: TerminalSettings.shared.fontSize, weight: .regular) suggestionLabel?.textColor = theme.suggestion suggestionLabel?.font = textView?.font } func startShell() { shellTask = Process() inputPipe = Pipe() outputPipe = Pipe() shellTask?.executableURL = URL(fileURLWithPath: "/bin/zsh") shellTask?.arguments = ["-l"] // Login shell for proper initialization // Better environment setup var env = ProcessInfo.processInfo.environment env["TERM"] = "xterm-256color" env["PS1"] = "$ " env["PROMPT"] = "$ " env["DISABLE_AUTO_TITLE"] = "true" shellTask?.environment = env shellTask?.standardInput = inputPipe shellTask?.standardOutput = outputPipe shellTask?.standardError = outputPipe outputPipe?.fileHandleForReading.readabilityHandler = { [weak self] handle in let data = handle.availableData if data.count > 0, let output = String(data: data, encoding: .utf8) { DispatchQueue.main.async { self?.appendText(output) } } } do { try shellTask?.run() appendText("Enhanced Terminal v3.0\n") appendText("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n") appendText("Features:\n") appendText(" • Cmd+T: New tab\n") appendText(" • Cmd+R: Search history (fuzzy search)\n") appendText(" • Cmd+K: AI command generator\n") appendText(" • Cmd+P: Command snippets\n") appendText(" • Tab: Accept auto-suggestion\n") appendText(" • Type 'help' for more commands\n") appendText("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n") // Wait a bit for shell to initialize, then show prompt DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in self?.showPrompt() } } catch { appendText("Error starting shell: \(error)\n") } } func showPrompt() { appendText("$ ") promptPosition = textView.string.count } func appendText(_ text: String) { let cleaned = cleanANSI(text) let theme = TerminalSettings.shared.theme let attr = NSAttributedString(string: cleaned, attributes: [ .foregroundColor: theme.foreground, .font: NSFont(name: TerminalSettings.shared.fontName, size: TerminalSettings.shared.fontSize) ?? NSFont.monospacedSystemFont(ofSize: TerminalSettings.shared.fontSize, weight: .regular) ]) textView.textStorage?.append(attr) textView.scrollToEndOfDocument(nil) } private func cleanANSI(_ text: String) -> String { var cleaned = text // Remove ANSI escape sequences let patterns = [ "\u{1B}\\[[0-9;]*[a-zA-Z]", "\u{1B}\\][0-9];[^\u{07}]*\u{07}", "\\]633;[^\u{07}]*\u{07}", "\\]697;[^\u{07}]*\u{07}", "\\]1337;[^\u{07}]*\u{07}", "\\]1;[^\u{07}]*\u{07}", "\\]2;[^\u{07}]*\u{07}", "\\]7;[^\u{07}]*\\\\", "\u{1B}\\]0;[^\u{07}]*\u{07}" ] for pattern in patterns { cleaned = cleaned.replacingOccurrences(of: pattern, with: "", options: .regularExpression) } return cleaned } func textView(_ textView: NSTextView, shouldChangeTextIn range: NSRange, replacementString: String?) -> Bool { // Don't allow editing before the prompt if range.location < promptPosition { return false } // Handle Enter key if replacementString == "\n" { let command = getCurrentCommand() executeCommand(command) hideSuggestion() return false } // Handle Tab key for accepting suggestions if replacementString == "\t" { if let suggestion = currentSuggestion { acceptSuggestion(suggestion) return false } } // Update suggestions after text changes DispatchQueue.main.async { [weak self] in self?.updateSuggestion() } return true } private func getCurrentCommand() -> String { guard promptPosition <= textView.string.count else { return "" } let startIndex = textView.string.index(textView.string.startIndex, offsetBy: promptPosition) return String(textView.string[startIndex...]) } private func updateSuggestion() { guard TerminalSettings.shared.enableAutoSuggestions else { hideSuggestion() return } let partial = getCurrentCommand() guard !partial.isEmpty else { hideSuggestion() return } if let suggestion = HistoryManager.shared.getSuggestion(for: partial) { showSuggestion(suggestion, for: partial) } else { hideSuggestion() } } private func showSuggestion(_ fullCommand: String, for partial: String) { guard let label = suggestionLabel else { return } currentSuggestion = fullCommand // Get the remaining part of the suggestion let remaining = String(fullCommand.dropFirst(partial.count)) label.stringValue = remaining // Position the label at the cursor if let layoutManager = textView.layoutManager, let textContainer = textView.textContainer { let glyphRange = layoutManager.glyphRange(for: textContainer) let lastGlyphIndex = layoutManager.glyphIndexForCharacter(at: textView.string.count) if lastGlyphIndex < glyphRange.upperBound { let rect = layoutManager.boundingRect(forGlyphRange: NSRange(location: lastGlyphIndex, length: 0), in: textContainer) label.frame = NSRect(x: rect.maxX, y: rect.minY, width: 400, height: rect.height) label.isHidden = false } } } private func hideSuggestion() { currentSuggestion = nil suggestionLabel?.isHidden = true } private func acceptSuggestion(_ suggestion: String) { let partial = getCurrentCommand() let remaining = String(suggestion.dropFirst(partial.count)) textView.insertText(remaining, replacementRange: NSRange(location: textView.string.count, length: 0)) hideSuggestion() } func executeCommand(_ command: String) { appendText("\n") let trimmed = command.trimmingCharacters(in: .whitespacesAndNewlines) // Handle built-in commands if trimmed == "exit" { NSApp.terminate(nil) return } if trimmed == "clear" { textView.string = "" showPrompt() return } if trimmed == "help" { showHelp() showPrompt() return } // Add to history if !trimmed.isEmpty { HistoryManager.shared.addCommand(trimmed) // Send to shell if let data = (trimmed + "\n").data(using: .utf8) { inputPipe?.fileHandleForWriting.write(data) } } // Show prompt after a delay DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in self?.showPrompt() } } private func showHelp() { appendText(""" Enhanced Terminal Commands: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Keyboard Shortcuts: • Cmd+T - New tab • Cmd+W - Close tab • Cmd+R - Search command history (fuzzy search) • Cmd+K - AI command generator • Cmd+P - Command snippets • Cmd+, - Settings • Tab - Accept auto-suggestion • Cmd+Plus - Increase font size • Cmd+Minus - Decrease font size Built-in Commands: • clear - Clear screen • help - Show this help • exit - Quit application ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ """) } func insertCommand(_ command: String) { textView.insertText(command, replacementRange: NSRange(location: textView.string.count, length: 0)) textView.window?.makeFirstResponder(textView) } func cleanup() { shellTask?.terminate() outputPipe?.fileHandleForReading.readabilityHandler = nil } // MARK: - NSMenuDelegate func menuNeedsUpdate(_ menu: NSMenu) { // Enable "Save as Snippet" only if there's selected text if let saveSnippetItem = menu.items.first(where: { $0.action == #selector(saveSelectionAsSnippet) }) { let hasSelection = textView.selectedRange().length > 0 saveSnippetItem.isEnabled = hasSelection } } deinit { cleanup() } } // MARK: - Tab Manager class TabManager { var tabs: [TerminalSession] = [] var currentIndex: Int = 0 func addTab(title: String = "Terminal") -> TerminalSession { let session = TerminalSession(title: "\(title) \(tabs.count + 1)") tabs.append(session) currentIndex = tabs.count - 1 return session } func removeTab(at index: Int) { guard index < tabs.count else { return } tabs[index].cleanup() tabs.remove(at: index) if currentIndex >= tabs.count { currentIndex = max(0, tabs.count - 1) } } func selectTab(at index: Int) { guard index < tabs.count else { return } currentIndex = index } func getCurrentSession() -> TerminalSession? { guard currentIndex < tabs.count else { return nil } return tabs[currentIndex] } }