- Split Panes: Cmd+D (vertical) and Cmd+Shift+D (horizontal) - Close panes with Cmd+W when multiple panes exist - Fixed alias matching to work with command arguments - Aliases now match first word only (e.g., 'gs status' works with gs=git) Split panes allow multiple terminal sessions side-by-side in the same tab
688 lines
27 KiB
Swift
688 lines
27 KiB
Swift
import Cocoa
|
||
|
||
class MainWindowController: NSWindowController {
|
||
var tabManager = TabManager()
|
||
var tabBar: NSStackView!
|
||
var contentView: NSView!
|
||
var splitView: NSSplitView?
|
||
var activePaneIndex: Int = 0
|
||
var panes: [TerminalSession] = []
|
||
var historyPanel: HistorySearchPanel!
|
||
var snippetPanel: SnippetPanel!
|
||
var aiPanel: AIGeneratorPanel!
|
||
var settingsWindow: SettingsWindowController!
|
||
|
||
convenience init() {
|
||
let window = NSWindow(
|
||
contentRect: NSRect(x: 0, y: 0, width: 1000, height: 700),
|
||
styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
||
backing: .buffered,
|
||
defer: false
|
||
)
|
||
window.titlebarAppearsTransparent = false
|
||
window.titleVisibility = .visible
|
||
window.center()
|
||
window.title = "Enhanced Terminal"
|
||
window.isReleasedWhenClosed = false
|
||
|
||
self.init(window: window)
|
||
setupUI()
|
||
setupPanels()
|
||
setupNotifications()
|
||
|
||
// Restore previous session or create new tab
|
||
restoreSession()
|
||
|
||
// Force window to show
|
||
window.makeKeyAndOrderFront(nil)
|
||
window.orderFrontRegardless()
|
||
|
||
// Debug: Print tab bar info
|
||
NSLog("Tab bar frame: \(tabBar.frame)")
|
||
NSLog("Tab bar subviews: \(tabBar.arrangedSubviews.count)")
|
||
NSLog("Window content view: \(window.contentView?.frame ?? .zero)")
|
||
}
|
||
|
||
private func restoreSession() {
|
||
let savedSessions = tabManager.loadSession()
|
||
|
||
if savedSessions.isEmpty {
|
||
// No saved session, create default tab
|
||
addNewTab()
|
||
} else {
|
||
// Restore saved tabs
|
||
for sessionData in savedSessions {
|
||
let session = tabManager.addTab(title: sessionData.title)
|
||
session.title = sessionData.title // Override the auto-generated title
|
||
session.setupTextView(frame: contentView.bounds)
|
||
|
||
// Restore working directory if available
|
||
if let workingDir = sessionData.workingDirectory {
|
||
session.currentWorkingDirectory = workingDir
|
||
}
|
||
|
||
session.startShell()
|
||
|
||
// Change to saved directory
|
||
if let workingDir = sessionData.workingDirectory, workingDir != "~" {
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||
session.executeCommand("cd \(workingDir)")
|
||
}
|
||
}
|
||
}
|
||
|
||
// Restore current tab index
|
||
let savedIndex = tabManager.getSavedCurrentIndex()
|
||
updateTabBar()
|
||
switchToTab(index: min(savedIndex, tabManager.tabs.count - 1))
|
||
}
|
||
}
|
||
|
||
private func setupUI() {
|
||
guard let window = window else { return }
|
||
|
||
let mainContainer = NSView(frame: window.contentView!.bounds)
|
||
mainContainer.autoresizingMask = [.width, .height]
|
||
|
||
// Tab bar - positioned below the title bar
|
||
let tabBarHeight: CGFloat = 28
|
||
let topMargin: CGFloat = 10 // Space below title bar
|
||
tabBar = NSStackView(frame: NSRect(x: 0, y: mainContainer.bounds.height - tabBarHeight - topMargin, width: mainContainer.bounds.width, height: tabBarHeight))
|
||
tabBar.orientation = .horizontal
|
||
tabBar.distribution = .fill
|
||
tabBar.alignment = .centerY
|
||
tabBar.spacing = 5
|
||
tabBar.autoresizingMask = [.width, .minYMargin]
|
||
tabBar.wantsLayer = true
|
||
tabBar.layer?.backgroundColor = NSColor(white: 0.15, alpha: 1.0).cgColor // Dark gray background
|
||
tabBar.edgeInsets = NSEdgeInsets(top: 2, left: 10, bottom: 2, right: 10)
|
||
|
||
NSLog("Creating tab bar at: \(tabBar.frame)")
|
||
|
||
// Content view below tab bar
|
||
contentView = NSView(frame: NSRect(x: 0, y: 0, width: mainContainer.bounds.width, height: mainContainer.bounds.height - tabBarHeight - topMargin))
|
||
contentView.autoresizingMask = [.width, .height]
|
||
contentView.wantsLayer = true
|
||
contentView.layer?.backgroundColor = NSColor.black.cgColor
|
||
|
||
NSLog("Creating content view at: \(contentView.frame)")
|
||
|
||
mainContainer.addSubview(contentView)
|
||
mainContainer.addSubview(tabBar)
|
||
|
||
window.contentView = mainContainer
|
||
|
||
NSLog("Main container bounds: \(mainContainer.bounds)")
|
||
|
||
setupMenuBar()
|
||
}
|
||
|
||
private func setupPanels() {
|
||
historyPanel = HistorySearchPanel()
|
||
snippetPanel = SnippetPanel()
|
||
aiPanel = AIGeneratorPanel()
|
||
settingsWindow = SettingsWindowController()
|
||
}
|
||
|
||
private func setupNotifications() {
|
||
NotificationCenter.default.addObserver(self, selector: #selector(themeChanged), name: NSNotification.Name("ThemeChanged"), object: nil)
|
||
}
|
||
|
||
@objc private func themeChanged() {
|
||
tabManager.tabs.forEach { $0.applyTheme() }
|
||
}
|
||
|
||
private func setupMenuBar() {
|
||
let mainMenu = NSMenu()
|
||
|
||
// App menu
|
||
let appMenuItem = NSMenuItem()
|
||
mainMenu.addItem(appMenuItem)
|
||
let appMenu = NSMenu()
|
||
appMenuItem.submenu = appMenu
|
||
|
||
appMenu.addItem(NSMenuItem(title: "About Enhanced Terminal", action: #selector(showAbout), keyEquivalent: ""))
|
||
appMenu.addItem(NSMenuItem.separator())
|
||
appMenu.addItem(NSMenuItem(title: "Settings...", action: #selector(openSettings), keyEquivalent: ","))
|
||
appMenu.addItem(NSMenuItem.separator())
|
||
appMenu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
|
||
|
||
// File menu
|
||
let fileMenuItem = NSMenuItem(title: "File", action: nil, keyEquivalent: "")
|
||
mainMenu.addItem(fileMenuItem)
|
||
let fileMenu = NSMenu(title: "File")
|
||
fileMenuItem.submenu = fileMenu
|
||
|
||
fileMenu.addItem(NSMenuItem(title: "New Tab", action: #selector(addNewTab), keyEquivalent: "t"))
|
||
fileMenu.addItem(NSMenuItem(title: "Close Tab", action: #selector(closeCurrentTab), keyEquivalent: "w"))
|
||
fileMenu.addItem(NSMenuItem.separator())
|
||
fileMenu.addItem(NSMenuItem(title: "Split Pane Vertically", action: #selector(splitPaneVertically), keyEquivalent: "d"))
|
||
fileMenu.addItem(NSMenuItem(title: "Split Pane Horizontally", action: #selector(splitPaneHorizontally), keyEquivalent: "D"))
|
||
fileMenu.addItem(NSMenuItem(title: "Close Pane", action: #selector(closePane), keyEquivalent: "w"))
|
||
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"))
|
||
|
||
// Edit menu
|
||
let editMenuItem = NSMenuItem(title: "Edit", action: nil, keyEquivalent: "")
|
||
mainMenu.addItem(editMenuItem)
|
||
let editMenu = NSMenu(title: "Edit")
|
||
editMenuItem.submenu = editMenu
|
||
|
||
editMenu.addItem(NSMenuItem(title: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c"))
|
||
editMenu.addItem(NSMenuItem(title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v"))
|
||
editMenu.addItem(NSMenuItem(title: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a"))
|
||
editMenu.addItem(NSMenuItem.separator())
|
||
editMenu.addItem(NSMenuItem(title: "Find...", action: #selector(showFindPanel), keyEquivalent: "f"))
|
||
editMenu.addItem(NSMenuItem.separator())
|
||
|
||
let searchHistoryItem = NSMenuItem(title: "Search History", action: #selector(searchHistory), keyEquivalent: "r")
|
||
searchHistoryItem.keyEquivalentModifierMask = [.command]
|
||
editMenu.addItem(searchHistoryItem)
|
||
|
||
// View menu
|
||
let viewMenuItem = NSMenuItem(title: "View", action: nil, keyEquivalent: "")
|
||
mainMenu.addItem(viewMenuItem)
|
||
let viewMenu = NSMenu(title: "View")
|
||
viewMenuItem.submenu = viewMenu
|
||
|
||
viewMenu.addItem(NSMenuItem(title: "Toggle Fullscreen", action: #selector(NSWindow.toggleFullScreen(_:)), keyEquivalent: "f"))
|
||
viewMenu.addItem(NSMenuItem.separator())
|
||
viewMenu.addItem(NSMenuItem(title: "Increase Font Size", action: #selector(increaseFontSize), keyEquivalent: "+"))
|
||
viewMenu.addItem(NSMenuItem(title: "Decrease Font Size", action: #selector(decreaseFontSize), keyEquivalent: "-"))
|
||
viewMenu.addItem(NSMenuItem(title: "Reset Font Size", action: #selector(resetFontSize), keyEquivalent: "0"))
|
||
|
||
// Tools menu
|
||
let toolsMenuItem = NSMenuItem(title: "Tools", action: nil, keyEquivalent: "")
|
||
mainMenu.addItem(toolsMenuItem)
|
||
let toolsMenu = NSMenu(title: "Tools")
|
||
toolsMenuItem.submenu = toolsMenu
|
||
|
||
toolsMenu.addItem(NSMenuItem(title: "AI Command Generator", action: #selector(showAIGenerator), keyEquivalent: "k"))
|
||
toolsMenu.addItem(NSMenuItem(title: "Command Snippets", action: #selector(showSnippets), keyEquivalent: "p"))
|
||
toolsMenu.addItem(NSMenuItem.separator())
|
||
toolsMenu.addItem(NSMenuItem(title: "Keyboard Shortcuts", action: #selector(showShortcuts), keyEquivalent: "?"))
|
||
toolsMenu.addItem(NSMenuItem.separator())
|
||
toolsMenu.addItem(NSMenuItem(title: "Clear History", action: #selector(clearHistory), keyEquivalent: ""))
|
||
|
||
// Window menu
|
||
let windowMenuItem = NSMenuItem(title: "Window", action: nil, keyEquivalent: "")
|
||
mainMenu.addItem(windowMenuItem)
|
||
let windowMenu = NSMenu(title: "Window")
|
||
windowMenuItem.submenu = windowMenu
|
||
|
||
windowMenu.addItem(NSMenuItem(title: "Minimize", action: #selector(NSWindow.miniaturize(_:)), keyEquivalent: "m"))
|
||
windowMenu.addItem(NSMenuItem(title: "Zoom", action: #selector(NSWindow.zoom(_:)), keyEquivalent: ""))
|
||
|
||
NSApp.mainMenu = mainMenu
|
||
}
|
||
|
||
@objc func addNewTab() {
|
||
let session = tabManager.addTab()
|
||
session.setupTextView(frame: contentView.bounds)
|
||
session.startShell()
|
||
|
||
updateTabBar()
|
||
switchToTab(index: tabManager.tabs.count - 1)
|
||
}
|
||
|
||
@objc func closeCurrentTab() {
|
||
guard tabManager.tabs.count > 1 else {
|
||
// If it's the last tab, quit the app
|
||
NSApp.terminate(nil)
|
||
return
|
||
}
|
||
|
||
let alert = NSAlert()
|
||
alert.messageText = "Close Tab?"
|
||
alert.informativeText = "Are you sure you want to close this terminal session?"
|
||
alert.addButton(withTitle: "Close")
|
||
alert.addButton(withTitle: "Cancel")
|
||
alert.alertStyle = .warning
|
||
|
||
if alert.runModal() == .alertFirstButtonReturn {
|
||
tabManager.removeTab(at: tabManager.currentIndex)
|
||
updateTabBar()
|
||
switchToTab(index: min(tabManager.currentIndex, tabManager.tabs.count - 1))
|
||
}
|
||
}
|
||
|
||
private func updateTabBar() {
|
||
// Remove all existing views
|
||
tabBar.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||
|
||
// Add tab buttons
|
||
for (index, session) in tabManager.tabs.enumerated() {
|
||
// Create container for tab button and close button
|
||
let tabContainer = NSView()
|
||
tabContainer.wantsLayer = true
|
||
tabContainer.translatesAutoresizingMaskIntoConstraints = false
|
||
|
||
// Tab button
|
||
let tabButton = NSButton()
|
||
tabButton.title = session.title
|
||
tabButton.bezelStyle = .rounded
|
||
tabButton.tag = index
|
||
tabButton.target = self
|
||
tabButton.action = #selector(tabClicked(_:))
|
||
tabButton.font = NSFont.systemFont(ofSize: 11)
|
||
tabButton.translatesAutoresizingMaskIntoConstraints = false
|
||
|
||
// Add double-click for rename
|
||
let doubleClickGesture = NSClickGestureRecognizer(target: self, action: #selector(tabDoubleClicked(_:)))
|
||
doubleClickGesture.numberOfClicksRequired = 2
|
||
tabButton.addGestureRecognizer(doubleClickGesture)
|
||
|
||
// Highlight current tab
|
||
if index == tabManager.currentIndex {
|
||
tabButton.contentTintColor = .systemBlue
|
||
tabButton.isBordered = true
|
||
} else {
|
||
tabButton.contentTintColor = .systemGray
|
||
tabButton.isBordered = false
|
||
}
|
||
|
||
// Close button (X)
|
||
let closeButton = NSButton()
|
||
closeButton.title = "×"
|
||
closeButton.bezelStyle = .rounded
|
||
closeButton.tag = index
|
||
closeButton.target = self
|
||
closeButton.action = #selector(closeTabAtIndex(_:))
|
||
closeButton.font = NSFont.systemFont(ofSize: 13, weight: .bold)
|
||
closeButton.translatesAutoresizingMaskIntoConstraints = false
|
||
|
||
tabContainer.addSubview(tabButton)
|
||
tabContainer.addSubview(closeButton)
|
||
|
||
// Layout constraints
|
||
NSLayoutConstraint.activate([
|
||
tabButton.leadingAnchor.constraint(equalTo: tabContainer.leadingAnchor),
|
||
tabButton.centerYAnchor.constraint(equalTo: tabContainer.centerYAnchor),
|
||
tabButton.widthAnchor.constraint(equalToConstant: 60),
|
||
tabButton.heightAnchor.constraint(equalToConstant: 20),
|
||
|
||
closeButton.leadingAnchor.constraint(equalTo: tabButton.trailingAnchor, constant: 2),
|
||
closeButton.centerYAnchor.constraint(equalTo: tabContainer.centerYAnchor),
|
||
closeButton.widthAnchor.constraint(equalToConstant: 18),
|
||
closeButton.heightAnchor.constraint(equalToConstant: 20),
|
||
closeButton.trailingAnchor.constraint(equalTo: tabContainer.trailingAnchor),
|
||
|
||
tabContainer.heightAnchor.constraint(equalToConstant: 20)
|
||
])
|
||
|
||
// Prevent container from stretching
|
||
tabContainer.setContentHuggingPriority(.init(1000), for: .horizontal)
|
||
tabContainer.setContentCompressionResistancePriority(.init(1000), for: .horizontal)
|
||
|
||
tabBar.addArrangedSubview(tabContainer)
|
||
}
|
||
|
||
// Add new tab button
|
||
let newTabButton = NSButton()
|
||
newTabButton.title = "+"
|
||
newTabButton.bezelStyle = .rounded
|
||
newTabButton.target = self
|
||
newTabButton.action = #selector(addNewTab)
|
||
newTabButton.font = NSFont.systemFont(ofSize: 13)
|
||
newTabButton.translatesAutoresizingMaskIntoConstraints = false
|
||
newTabButton.setContentHuggingPriority(.init(1000), for: .horizontal)
|
||
|
||
NSLayoutConstraint.activate([
|
||
newTabButton.widthAnchor.constraint(equalToConstant: 22),
|
||
newTabButton.heightAnchor.constraint(equalToConstant: 20)
|
||
])
|
||
|
||
tabBar.addArrangedSubview(newTabButton)
|
||
|
||
// Add spacer at the end to push everything to the left
|
||
let spacer = NSView()
|
||
spacer.setContentHuggingPriority(.init(1), for: .horizontal)
|
||
tabBar.addArrangedSubview(spacer)
|
||
|
||
// Force layout update
|
||
tabBar.needsLayout = true
|
||
tabBar.layoutSubtreeIfNeeded()
|
||
}
|
||
|
||
@objc func closeTabAtIndex(_ sender: NSButton) {
|
||
let index = sender.tag
|
||
|
||
// Don't close if it's the last tab
|
||
guard tabManager.tabs.count > 1 else {
|
||
NSApp.terminate(nil)
|
||
return
|
||
}
|
||
|
||
// Confirm if there's content
|
||
let alert = NSAlert()
|
||
alert.messageText = "Close Tab?"
|
||
alert.informativeText = "Are you sure you want to close this tab?"
|
||
alert.addButton(withTitle: "Close")
|
||
alert.addButton(withTitle: "Cancel")
|
||
alert.alertStyle = .warning
|
||
|
||
if alert.runModal() == .alertFirstButtonReturn {
|
||
tabManager.removeTab(at: index)
|
||
|
||
// Switch to previous tab or first tab
|
||
let newIndex = min(index, tabManager.tabs.count - 1)
|
||
switchToTab(index: newIndex)
|
||
}
|
||
}
|
||
|
||
@objc func tabClicked(_ sender: NSButton) {
|
||
switchToTab(index: sender.tag)
|
||
}
|
||
|
||
@objc func tabDoubleClicked(_ sender: NSClickGestureRecognizer) {
|
||
guard let button = sender.view as? NSButton else { return }
|
||
let index = button.tag
|
||
|
||
let alert = NSAlert()
|
||
alert.messageText = "Rename Tab"
|
||
alert.informativeText = "Enter a new name for this tab:"
|
||
|
||
let textField = NSTextField(frame: NSRect(x: 0, y: 0, width: 200, height: 24))
|
||
textField.stringValue = tabManager.tabs[index].title
|
||
textField.placeholderString = "Tab name"
|
||
|
||
alert.accessoryView = textField
|
||
alert.addButton(withTitle: "Rename")
|
||
alert.addButton(withTitle: "Cancel")
|
||
|
||
alert.window.initialFirstResponder = textField
|
||
|
||
if alert.runModal() == .alertFirstButtonReturn {
|
||
let newName = textField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
if !newName.isEmpty {
|
||
tabManager.tabs[index].title = newName
|
||
updateTabBar()
|
||
tabManager.saveSession()
|
||
}
|
||
}
|
||
}
|
||
|
||
private func switchToTab(index: Int) {
|
||
guard index < tabManager.tabs.count else { return }
|
||
|
||
// Remove current view from content view (but don't destroy it)
|
||
contentView.subviews.forEach { $0.removeFromSuperview() }
|
||
|
||
// Select the new tab
|
||
tabManager.selectTab(at: index)
|
||
tabManager.saveSession()
|
||
|
||
// Add the selected tab's scroll view to content view
|
||
if let session = tabManager.getCurrentSession() {
|
||
session.scrollView.frame = contentView.bounds
|
||
session.scrollView.autoresizingMask = [.width, .height]
|
||
contentView.addSubview(session.scrollView)
|
||
|
||
// Make the text view first responder
|
||
window?.makeFirstResponder(session.textView)
|
||
}
|
||
|
||
updateTabBar()
|
||
}
|
||
|
||
@objc func searchHistory() {
|
||
guard let window = window else { return }
|
||
historyPanel.show(relativeTo: window) { [weak self] command in
|
||
if let command = command, let session = self?.tabManager.getCurrentSession() {
|
||
session.insertCommand(command)
|
||
}
|
||
}
|
||
}
|
||
|
||
@objc func showSnippets() {
|
||
guard let window = window else { return }
|
||
snippetPanel.show(relativeTo: window) { [weak self] snippet in
|
||
if let snippet = snippet, let session = self?.tabManager.getCurrentSession() {
|
||
session.insertCommand(snippet.command)
|
||
}
|
||
}
|
||
}
|
||
|
||
@objc func showAIGenerator() {
|
||
guard AIManager.shared.isAvailable() else {
|
||
let alert = NSAlert()
|
||
alert.messageText = "AI Not Configured"
|
||
alert.informativeText = "Please configure an AI provider in Settings (Cmd+,) first.\n\nYou can use:\n• OpenAI (requires API key)\n• Ollama (free, local)"
|
||
alert.alertStyle = .informational
|
||
alert.addButton(withTitle: "Open Settings")
|
||
alert.addButton(withTitle: "Cancel")
|
||
|
||
if alert.runModal() == .alertFirstButtonReturn {
|
||
openSettings()
|
||
}
|
||
return
|
||
}
|
||
|
||
guard let window = window else { return }
|
||
aiPanel.show(relativeTo: window) { [weak self] command in
|
||
if let command = command, let session = self?.tabManager.getCurrentSession() {
|
||
session.insertCommand(command)
|
||
}
|
||
}
|
||
}
|
||
|
||
@objc func openSettings() {
|
||
settingsWindow.showWindow(nil)
|
||
}
|
||
|
||
@objc func clearScreen() {
|
||
if let session = tabManager.getCurrentSession() {
|
||
session.textView.string = ""
|
||
session.showPrompt()
|
||
}
|
||
}
|
||
|
||
@objc func clearScrollback() {
|
||
if let session = tabManager.getCurrentSession() {
|
||
let alert = NSAlert()
|
||
alert.messageText = "Clear Scrollback?"
|
||
alert.informativeText = "This will clear all terminal output history."
|
||
alert.addButton(withTitle: "Clear")
|
||
alert.addButton(withTitle: "Cancel")
|
||
alert.alertStyle = .warning
|
||
|
||
if alert.runModal() == .alertFirstButtonReturn {
|
||
session.textView.string = ""
|
||
session.showPrompt()
|
||
}
|
||
}
|
||
}
|
||
|
||
@objc func showFindPanel() {
|
||
if let session = tabManager.getCurrentSession() {
|
||
session.textView.performTextFinderAction(nil)
|
||
}
|
||
}
|
||
|
||
@objc func showShortcuts() {
|
||
let alert = NSAlert()
|
||
alert.messageText = "Keyboard Shortcuts"
|
||
alert.informativeText = """
|
||
File:
|
||
Cmd+T - New Tab
|
||
Cmd+W - Close Tab
|
||
Cmd+D - Split Pane Vertically
|
||
Cmd+Shift+D - Split Pane Horizontally
|
||
Cmd+K - Clear Screen
|
||
Cmd+Shift+K - Clear Scrollback
|
||
|
||
Edit:
|
||
Cmd+C - Copy
|
||
Cmd+V - Paste
|
||
Cmd+A - Select All
|
||
Cmd+F - Find in Terminal
|
||
Cmd+R - Search History
|
||
|
||
Tools:
|
||
Cmd+K - AI Command Generator
|
||
Cmd+P - Command Snippets
|
||
Cmd+, - Settings
|
||
Cmd+? - This Help
|
||
|
||
View:
|
||
Cmd+Plus - Increase Font
|
||
Cmd+Minus - Decrease Font
|
||
Cmd+0 - Reset Font
|
||
Cmd+F - Toggle Fullscreen
|
||
|
||
Terminal:
|
||
Tab - Accept Auto-suggestion
|
||
Double-click Tab - Rename Tab
|
||
"""
|
||
alert.alertStyle = .informational
|
||
alert.addButton(withTitle: "OK")
|
||
alert.runModal()
|
||
}
|
||
|
||
@objc func splitPaneVertically() {
|
||
guard let currentSession = tabManager.getCurrentSession() else { return }
|
||
|
||
// Create split view if it doesn't exist
|
||
if splitView == nil {
|
||
splitView = NSSplitView(frame: contentView.bounds)
|
||
splitView?.isVertical = true
|
||
splitView?.dividerStyle = .thin
|
||
splitView?.autoresizingMask = [.width, .height]
|
||
|
||
// Move current session to split view
|
||
currentSession.scrollView.removeFromSuperview()
|
||
splitView?.addArrangedSubview(currentSession.scrollView)
|
||
panes = [currentSession]
|
||
}
|
||
|
||
// Create new pane
|
||
let newSession = TerminalSession(title: "Pane \(panes.count + 1)")
|
||
newSession.setupTextView(frame: CGRect(x: 0, y: 0, width: contentView.bounds.width / 2, height: contentView.bounds.height))
|
||
newSession.startShell()
|
||
|
||
splitView?.addArrangedSubview(newSession.scrollView)
|
||
panes.append(newSession)
|
||
activePaneIndex = panes.count - 1
|
||
|
||
// Add split view to content if not already there
|
||
if splitView?.superview == nil {
|
||
contentView.subviews.forEach { $0.removeFromSuperview() }
|
||
contentView.addSubview(splitView!)
|
||
}
|
||
|
||
newSession.textView.window?.makeFirstResponder(newSession.textView)
|
||
}
|
||
|
||
@objc func splitPaneHorizontally() {
|
||
guard let currentSession = tabManager.getCurrentSession() else { return }
|
||
|
||
// Create split view if it doesn't exist
|
||
if splitView == nil {
|
||
splitView = NSSplitView(frame: contentView.bounds)
|
||
splitView?.isVertical = false
|
||
splitView?.dividerStyle = .thin
|
||
splitView?.autoresizingMask = [.width, .height]
|
||
|
||
// Move current session to split view
|
||
currentSession.scrollView.removeFromSuperview()
|
||
splitView?.addArrangedSubview(currentSession.scrollView)
|
||
panes = [currentSession]
|
||
}
|
||
|
||
// Create new pane
|
||
let newSession = TerminalSession(title: "Pane \(panes.count + 1)")
|
||
newSession.setupTextView(frame: CGRect(x: 0, y: 0, width: contentView.bounds.width, height: contentView.bounds.height / 2))
|
||
newSession.startShell()
|
||
|
||
splitView?.addArrangedSubview(newSession.scrollView)
|
||
panes.append(newSession)
|
||
activePaneIndex = panes.count - 1
|
||
|
||
// Add split view to content if not already there
|
||
if splitView?.superview == nil {
|
||
contentView.subviews.forEach { $0.removeFromSuperview() }
|
||
contentView.addSubview(splitView!)
|
||
}
|
||
|
||
newSession.textView.window?.makeFirstResponder(newSession.textView)
|
||
}
|
||
|
||
@objc func closePane() {
|
||
guard panes.count > 1, activePaneIndex < panes.count else { return }
|
||
|
||
let pane = panes[activePaneIndex]
|
||
pane.cleanup()
|
||
pane.scrollView.removeFromSuperview()
|
||
panes.remove(at: activePaneIndex)
|
||
|
||
if panes.count == 1 {
|
||
// Only one pane left, remove split view
|
||
splitView?.removeFromSuperview()
|
||
splitView = nil
|
||
|
||
let remainingSession = panes[0]
|
||
remainingSession.scrollView.removeFromSuperview()
|
||
remainingSession.scrollView.frame = contentView.bounds
|
||
contentView.addSubview(remainingSession.scrollView)
|
||
panes = []
|
||
}
|
||
|
||
activePaneIndex = min(activePaneIndex, panes.count - 1)
|
||
}
|
||
|
||
@objc func clearHistory() {
|
||
let alert = NSAlert()
|
||
alert.messageText = "Clear Command History?"
|
||
alert.informativeText = "This will permanently delete all command history."
|
||
alert.addButton(withTitle: "Clear")
|
||
alert.addButton(withTitle: "Cancel")
|
||
alert.alertStyle = .warning
|
||
|
||
if alert.runModal() == .alertFirstButtonReturn {
|
||
HistoryManager.shared.clearHistory()
|
||
}
|
||
}
|
||
|
||
@objc func increaseFontSize() {
|
||
TerminalSettings.shared.fontSize += 1
|
||
tabManager.tabs.forEach { $0.applyTheme() }
|
||
}
|
||
|
||
@objc func decreaseFontSize() {
|
||
TerminalSettings.shared.fontSize = max(8, TerminalSettings.shared.fontSize - 1)
|
||
tabManager.tabs.forEach { $0.applyTheme() }
|
||
}
|
||
|
||
@objc func resetFontSize() {
|
||
TerminalSettings.shared.fontSize = 14
|
||
tabManager.tabs.forEach { $0.applyTheme() }
|
||
}
|
||
|
||
@objc func showAbout() {
|
||
let alert = NSAlert()
|
||
alert.messageText = "Enhanced Terminal v3.0"
|
||
alert.informativeText = """
|
||
A feature-rich macOS terminal emulator
|
||
|
||
Features:
|
||
• Multiple tabs
|
||
• Command history search (Cmd+R)
|
||
• Auto-suggestions
|
||
• AI command generation
|
||
• Command snippets
|
||
• Multiple themes
|
||
• Customizable settings
|
||
|
||
Built with Swift and Cocoa
|
||
"""
|
||
alert.alertStyle = .informational
|
||
alert.addButton(withTitle: "OK")
|
||
alert.runModal()
|
||
}
|
||
|
||
deinit {
|
||
NotificationCenter.default.removeObserver(self)
|
||
}
|
||
}
|