diff --git a/.DS_Store b/.DS_Store index e65d791..6c88134 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 6f35b1c..7eb7782 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 03ec769..2e06778 100644 --- a/MainWindowController.swift +++ b/MainWindowController.swift @@ -52,7 +52,20 @@ class MainWindowController: NSWindowController { 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 @@ -181,6 +194,8 @@ class MainWindowController: NSWindowController { 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 @@ -478,6 +493,44 @@ class MainWindowController: NSWindowController { } } + @objc func showShortcuts() { + let alert = NSAlert() + alert.messageText = "Keyboard Shortcuts" + alert.informativeText = """ + File: + Cmd+T - New Tab + Cmd+W - Close Tab + 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 clearHistory() { let alert = NSAlert() alert.messageText = "Clear Command History?" diff --git a/Panels.swift b/Panels.swift index 6e5ce4e..45fa7e5 100644 --- a/Panels.swift +++ b/Panels.swift @@ -769,8 +769,8 @@ class AIGeneratorPanel: NSPanel, NSTextFieldDelegate { class SettingsWindowController: NSWindowController { convenience init() { let window = NSWindow( - contentRect: NSRect(x: 0, y: 0, width: 500, height: 550), - styleMask: [.titled, .closable], + contentRect: NSRect(x: 0, y: 0, width: 500, height: 650), + styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: false ) @@ -907,8 +907,23 @@ class SettingsWindowController: NSWindowController { yPos -= 50 + // Command aliases + let aliasLabel = NSTextField(labelWithString: "Command Aliases:") + aliasLabel.frame = NSRect(x: 20, y: yPos, width: 120, height: 20) + + let aliasButton = NSButton(frame: NSRect(x: 150, y: yPos - 5, width: 150, height: 30)) + aliasButton.title = "Manage Aliases..." + aliasButton.bezelStyle = .rounded + aliasButton.target = self + aliasButton.action = #selector(manageAliases) + + container.addSubview(aliasLabel) + container.addSubview(aliasButton) + + yPos -= 50 + // Info label - let infoLabel = NSTextField(wrappingLabelWithString: "Note: For Ollama, make sure it's running locally. For OpenAI, you need an API key from platform.openai.com\n\nStartup commands will run automatically when opening a new tab.") + let infoLabel = NSTextField(wrappingLabelWithString: "Note: For Ollama, make sure it's running locally. For OpenAI, you need an API key from platform.openai.com\n\nStartup commands will run automatically when opening a new tab.\n\nAliases let you create shortcuts for long commands (e.g., 'gs' → 'git status').") infoLabel.frame = NSRect(x: 20, y: 20, width: 460, height: 40) infoLabel.textColor = .secondaryLabelColor infoLabel.font = NSFont.systemFont(ofSize: 11) @@ -964,4 +979,94 @@ class SettingsWindowController: NSWindowController { let commands = sender.stringValue.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) } TerminalSettings.shared.startupCommands = commands.filter { !$0.isEmpty } } + + @objc private func manageAliases() { + let panel = NSPanel( + contentRect: NSRect(x: 0, y: 0, width: 400, height: 300), + styleMask: [.titled, .closable, .resizable], + backing: .buffered, + defer: false + ) + panel.title = "Command Aliases" + panel.center() + + let container = NSView(frame: panel.contentView!.bounds) + container.autoresizingMask = [.width, .height] + + // Instructions + let instructions = NSTextField(wrappingLabelWithString: "Format: alias=command (one per line)\nExample: gs=git status") + instructions.frame = NSRect(x: 20, y: container.bounds.height - 50, width: 360, height: 40) + instructions.autoresizingMask = [.width, .minYMargin] + + // Text view for aliases + let scrollView = NSScrollView(frame: NSRect(x: 20, y: 60, width: 360, height: container.bounds.height - 120)) + scrollView.autoresizingMask = [.width, .height] + scrollView.hasVerticalScroller = true + scrollView.borderType = .bezelBorder + + let textView = NSTextView(frame: scrollView.bounds) + textView.autoresizingMask = [.width, .height] + textView.isEditable = true + textView.font = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular) + + // Load current aliases + let aliasText = TerminalSettings.shared.commandAliases.map { "\($0.key)=\($0.value)" }.joined(separator: "\n") + textView.string = aliasText + + scrollView.documentView = textView + + // Save button + let saveButton = NSButton(frame: NSRect(x: 300, y: 20, width: 80, height: 30)) + saveButton.title = "Save" + saveButton.bezelStyle = .rounded + saveButton.target = self + saveButton.action = #selector(saveAliases(_:)) + saveButton.tag = textView.hash + + // Cancel button + let cancelButton = NSButton(frame: NSRect(x: 210, y: 20, width: 80, height: 30)) + cancelButton.title = "Cancel" + cancelButton.bezelStyle = .rounded + cancelButton.target = panel + cancelButton.action = #selector(NSWindow.close) + + container.addSubview(instructions) + container.addSubview(scrollView) + container.addSubview(saveButton) + container.addSubview(cancelButton) + + panel.contentView = container + panel.makeKeyAndOrderFront(nil) + + // Store textView reference + objc_setAssociatedObject(saveButton, "textView", textView, .OBJC_ASSOCIATION_RETAIN) + } + + @objc private func saveAliases(_ sender: NSButton) { + guard let textView = objc_getAssociatedObject(sender, "textView") as? NSTextView else { return } + + var aliases: [String: String] = [:] + let lines = textView.string.components(separatedBy: .newlines) + + for line in lines { + let trimmed = line.trimmingCharacters(in: .whitespaces) + guard !trimmed.isEmpty else { continue } + + let parts = trimmed.split(separator: "=", maxSplits: 1) + if parts.count == 2 { + let alias = String(parts[0]).trimmingCharacters(in: .whitespaces) + let command = String(parts[1]).trimmingCharacters(in: .whitespaces) + aliases[alias] = command + } + } + + TerminalSettings.shared.commandAliases = aliases + sender.window?.close() + + let alert = NSAlert() + alert.messageText = "Aliases Saved" + alert.informativeText = "Saved \(aliases.count) alias(es). Type 'aliases' in terminal to see them." + alert.alertStyle = .informational + alert.runModal() + } } diff --git a/Settings.swift b/Settings.swift index fedbfbb..524d7b5 100644 --- a/Settings.swift +++ b/Settings.swift @@ -16,6 +16,7 @@ class TerminalSettings { var enableAutoSuggestions: Bool = true var maxHistorySize: Int = 1000 var startupCommands: [String] = [] + var commandAliases: [String: String] = [:] // alias -> command enum AIProvider: String, CaseIterable { case none = "None" diff --git a/TerminalSession.swift b/TerminalSession.swift index 294c76a..c0633ed 100644 --- a/TerminalSession.swift +++ b/TerminalSession.swift @@ -10,6 +10,7 @@ class TerminalSession: NSObject, NSTextViewDelegate, NSMenuDelegate { var promptPosition = 0 var sessionID: UUID var title: String + var currentWorkingDirectory: String = "~" private var currentSuggestion: String? init(title: String = "Terminal") { @@ -401,30 +402,51 @@ class TerminalSession: NSObject, NSTextViewDelegate, NSMenuDelegate { let trimmed = command.trimmingCharacters(in: .whitespacesAndNewlines) + // Check for aliases + var actualCommand = trimmed + if let aliasCommand = TerminalSettings.shared.commandAliases[trimmed] { + actualCommand = aliasCommand + appendText("(alias: \(aliasCommand))\n") + } + + // Track directory changes + if actualCommand.hasPrefix("cd ") { + let path = String(actualCommand.dropFirst(3)).trimmingCharacters(in: .whitespaces) + if !path.isEmpty { + currentWorkingDirectory = path + } + } + // Handle built-in commands - if trimmed == "exit" { + if actualCommand == "exit" { NSApp.terminate(nil) return } - if trimmed == "clear" { + if actualCommand == "clear" { textView.string = "" showPrompt() return } - if trimmed == "help" { + if actualCommand == "help" { showHelp() showPrompt() return } + if actualCommand == "aliases" { + showAliases() + showPrompt() + return + } + // Add to history if !trimmed.isEmpty { HistoryManager.shared.addCommand(trimmed) // Send to shell - if let data = (trimmed + "\n").data(using: .utf8) { + if let data = (actualCommand + "\n").data(using: .utf8) { inputPipe?.fileHandleForWriting.write(data) } } @@ -453,12 +475,27 @@ class TerminalSession: NSObject, NSTextViewDelegate, NSMenuDelegate { Built-in Commands: • clear - Clear screen • help - Show this help + • aliases - Show command aliases • exit - Quit application ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ """) } + private func showAliases() { + let aliases = TerminalSettings.shared.commandAliases + if aliases.isEmpty { + appendText("No aliases defined. Add them in Settings (Cmd+,)\n") + } else { + appendText("Command Aliases:\n") + appendText("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n") + for (alias, command) in aliases.sorted(by: { $0.key < $1.key }) { + appendText(" \(alias) → \(command)\n") + } + appendText("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n") + } + } + func insertCommand(_ command: String) { textView.insertText(command, replacementRange: NSRange(location: textView.string.count, length: 0)) textView.window?.makeFirstResponder(textView) @@ -491,6 +528,7 @@ class TabManager { struct SessionData: Codable { let title: String let sessionID: String + let workingDirectory: String? } func addTab(title: String = "Terminal") -> TerminalSession { @@ -522,7 +560,7 @@ class TabManager { } func saveSession() { - let sessionData = tabs.map { SessionData(title: $0.title, sessionID: $0.sessionID.uuidString) } + let sessionData = tabs.map { SessionData(title: $0.title, sessionID: $0.sessionID.uuidString, workingDirectory: $0.currentWorkingDirectory) } if let data = try? JSONEncoder().encode(sessionData) { UserDefaults.standard.set(data, forKey: "terminal_session") UserDefaults.standard.set(currentIndex, forKey: "terminal_current_tab")