Initial Push

This commit is contained in:
2026-01-31 23:01:54 +00:00
parent 44d036c8d6
commit 5874b31464
32 changed files with 7171 additions and 0 deletions

451
MainWindowController.swift Normal file
View File

@@ -0,0 +1,451 @@
import Cocoa
class MainWindowController: NSWindowController {
var tabManager = TabManager()
var tabBar: NSStackView!
var contentView: NSView!
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()
addNewTab()
// 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 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: "Clear Screen", action: #selector(clearScreen), 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())
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: "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, _) 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 = "Tab \(index + 1)"
tabButton.bezelStyle = .rounded
tabButton.tag = index
tabButton.target = self
tabButton.action = #selector(tabClicked(_:))
tabButton.font = NSFont.systemFont(ofSize: 11)
tabButton.translatesAutoresizingMaskIntoConstraints = false
// 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)
}
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)
// 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 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)
}
}