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:
Binary file not shown.
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user