diff --git a/.DS_Store b/.DS_Store index 0034b68..e85c1f5 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/EnhancedTerminal.app/Contents/MacOS/EnhancedTerminal b/EnhancedTerminal.app/Contents/MacOS/EnhancedTerminal index 759a01a..a0dd3c3 100755 Binary files a/EnhancedTerminal.app/Contents/MacOS/EnhancedTerminal and b/EnhancedTerminal.app/Contents/MacOS/EnhancedTerminal differ diff --git a/MainWindowController.swift b/MainWindowController.swift index 63ff880..a7115ec 100644 --- a/MainWindowController.swift +++ b/MainWindowController.swift @@ -50,6 +50,8 @@ class MainWindowController: NSWindowController { var snippetPanel: SnippetPanel! var aiPanel: AIGeneratorPanel! var settingsWindow: SettingsWindowController! + var zoomedPane: TerminalSession? + var preZoomSplitView: NSSplitView? convenience init() { let window = NSWindow( @@ -202,6 +204,10 @@ class MainWindowController: NSWindowController { 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")) @@ -552,6 +558,7 @@ class MainWindowController: NSWindowController { 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 @@ -561,6 +568,7 @@ class MainWindowController: NSWindowController { Cmd+A - Select All Cmd+F - Find in Terminal Cmd+R - Search History + Middle-click - Paste Tools: Cmd+K - AI Command Generator @@ -579,6 +587,8 @@ class MainWindowController: NSWindowController { 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") @@ -602,6 +612,7 @@ class MainWindowController: NSWindowController { 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() @@ -646,6 +657,7 @@ class MainWindowController: NSWindowController { 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() @@ -759,6 +771,53 @@ class MainWindowController: NSWindowController { } } + @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?" @@ -813,3 +872,19 @@ class MainWindowController: NSWindowController { 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 + } +} diff --git a/Settings.swift b/Settings.swift index 524d7b5..0baf7d1 100644 --- a/Settings.swift +++ b/Settings.swift @@ -17,6 +17,8 @@ class TerminalSettings { var maxHistorySize: Int = 1000 var startupCommands: [String] = [] var commandAliases: [String: String] = [:] // alias -> command + var copyOnSelect: Bool = true + var visualBell: Bool = true enum AIProvider: String, CaseIterable { case none = "None" diff --git a/TerminalSession.swift b/TerminalSession.swift index f94c9c7..9a1ee99 100644 --- a/TerminalSession.swift +++ b/TerminalSession.swift @@ -45,6 +45,32 @@ class TerminalSession: NSObject, NSTextViewDelegate, NSMenuDelegate { // Setup suggestion label setupSuggestionLabel() + + // Setup middle-click paste + setupMiddleClickPaste() + + // Setup clickable URLs + setupClickableURLs() + } + + private func setupMiddleClickPaste() { + let clickGesture = NSClickGestureRecognizer(target: self, action: #selector(handleMiddleClick(_:))) + clickGesture.buttonMask = 0x4 // Middle button + textView.addGestureRecognizer(clickGesture) + } + + @objc private func handleMiddleClick(_ gesture: NSClickGestureRecognizer) { + if let pasteString = NSPasteboard.general.string(forType: .string) { + textView.insertText(pasteString, replacementRange: textView.selectedRange()) + } + } + + private func setupClickableURLs() { + textView.isAutomaticLinkDetectionEnabled = true + textView.linkTextAttributes = [ + .foregroundColor: NSColor.systemBlue, + .underlineStyle: NSUnderlineStyle.single.rawValue + ] } private func setupContextMenu() { @@ -362,6 +388,13 @@ class TerminalSession: NSObject, NSTextViewDelegate, NSMenuDelegate { if selectedRange.location < promptPosition && selectedRange.length == 0 { textView.setSelectedRange(NSRange(location: promptPosition, length: 0)) } + + // Copy on select + if TerminalSettings.shared.copyOnSelect && selectedRange.length > 0 { + let selectedText = (textView.string as NSString).substring(with: selectedRange) + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(selectedText, forType: .string) + } } private func getCurrentCommand() -> String { @@ -555,12 +588,34 @@ class TerminalSession: NSObject, NSTextViewDelegate, NSMenuDelegate { } } + // Visual bell on command completion + if TerminalSettings.shared.visualBell { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + self?.flashVisualBell() + } + } + // Show prompt after a delay DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in self?.showPrompt() } } + private func flashVisualBell() { + guard let layer = textView.layer else { return } + let originalBg = layer.backgroundColor + + NSAnimationContext.runAnimationGroup({ context in + context.duration = 0.1 + layer.backgroundColor = NSColor.white.withAlphaComponent(0.1).cgColor + }, completionHandler: { + NSAnimationContext.runAnimationGroup({ context in + context.duration = 0.2 + layer.backgroundColor = originalBg + }) + }) + } + private func showHelp() { appendText(""" Enhanced Terminal Commands: