Add quick-win features: copy-on-select, middle-click paste, visual bell, zoom pane, clickable URLs, draggable dividers

- Copy on select: Auto-copy selected text to clipboard
- Middle-click paste: Quick paste with middle mouse button
- Visual bell: Subtle flash when commands complete
- Zoom pane: Cmd+Shift+Enter to maximize/restore split pane
- Clickable URLs: Cmd+click to open URLs in browser
- Draggable dividers: Resize split panes by dragging divider
- Updated help and keyboard shortcuts documentation
This commit is contained in:
2026-01-31 23:47:49 +00:00
parent faad3c3981
commit 56d6b31b2f
5 changed files with 132 additions and 0 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -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
}
}

View File

@@ -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"

View File

@@ -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: