Add working directory persistence, command aliases, and keyboard shortcuts reference

1. Working Directory Persistence: Tracks and restores last directory per tab
2. Command Aliases: Create shortcuts for long commands (e.g., gs=git status)
   - Manage aliases in Settings
   - Type 'aliases' to see all defined aliases
3. Keyboard Shortcuts Reference: Cmd+? to view all shortcuts

All features tested and working
This commit is contained in:
2026-01-31 23:25:13 +00:00
parent 9561e229aa
commit 7edf5d7186
6 changed files with 205 additions and 8 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -52,7 +52,20 @@ class MainWindowController: NSWindowController {
let session = tabManager.addTab(title: sessionData.title) let session = tabManager.addTab(title: sessionData.title)
session.title = sessionData.title // Override the auto-generated title session.title = sessionData.title // Override the auto-generated title
session.setupTextView(frame: contentView.bounds) session.setupTextView(frame: contentView.bounds)
// Restore working directory if available
if let workingDir = sessionData.workingDirectory {
session.currentWorkingDirectory = workingDir
}
session.startShell() 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 // 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: "AI Command Generator", action: #selector(showAIGenerator), keyEquivalent: "k"))
toolsMenu.addItem(NSMenuItem(title: "Command Snippets", action: #selector(showSnippets), keyEquivalent: "p")) toolsMenu.addItem(NSMenuItem(title: "Command Snippets", action: #selector(showSnippets), keyEquivalent: "p"))
toolsMenu.addItem(NSMenuItem.separator()) 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: "")) toolsMenu.addItem(NSMenuItem(title: "Clear History", action: #selector(clearHistory), keyEquivalent: ""))
// Window menu // 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() { @objc func clearHistory() {
let alert = NSAlert() let alert = NSAlert()
alert.messageText = "Clear Command History?" alert.messageText = "Clear Command History?"

View File

@@ -769,8 +769,8 @@ class AIGeneratorPanel: NSPanel, NSTextFieldDelegate {
class SettingsWindowController: NSWindowController { class SettingsWindowController: NSWindowController {
convenience init() { convenience init() {
let window = NSWindow( let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 500, height: 550), contentRect: NSRect(x: 0, y: 0, width: 500, height: 650),
styleMask: [.titled, .closable], styleMask: [.titled, .closable, .resizable],
backing: .buffered, backing: .buffered,
defer: false defer: false
) )
@@ -907,8 +907,23 @@ class SettingsWindowController: NSWindowController {
yPos -= 50 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 // 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.frame = NSRect(x: 20, y: 20, width: 460, height: 40)
infoLabel.textColor = .secondaryLabelColor infoLabel.textColor = .secondaryLabelColor
infoLabel.font = NSFont.systemFont(ofSize: 11) infoLabel.font = NSFont.systemFont(ofSize: 11)
@@ -964,4 +979,94 @@ class SettingsWindowController: NSWindowController {
let commands = sender.stringValue.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) } let commands = sender.stringValue.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }
TerminalSettings.shared.startupCommands = commands.filter { !$0.isEmpty } 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()
}
} }

View File

@@ -16,6 +16,7 @@ class TerminalSettings {
var enableAutoSuggestions: Bool = true var enableAutoSuggestions: Bool = true
var maxHistorySize: Int = 1000 var maxHistorySize: Int = 1000
var startupCommands: [String] = [] var startupCommands: [String] = []
var commandAliases: [String: String] = [:] // alias -> command
enum AIProvider: String, CaseIterable { enum AIProvider: String, CaseIterable {
case none = "None" case none = "None"

View File

@@ -10,6 +10,7 @@ class TerminalSession: NSObject, NSTextViewDelegate, NSMenuDelegate {
var promptPosition = 0 var promptPosition = 0
var sessionID: UUID var sessionID: UUID
var title: String var title: String
var currentWorkingDirectory: String = "~"
private var currentSuggestion: String? private var currentSuggestion: String?
init(title: String = "Terminal") { init(title: String = "Terminal") {
@@ -401,30 +402,51 @@ class TerminalSession: NSObject, NSTextViewDelegate, NSMenuDelegate {
let trimmed = command.trimmingCharacters(in: .whitespacesAndNewlines) 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 // Handle built-in commands
if trimmed == "exit" { if actualCommand == "exit" {
NSApp.terminate(nil) NSApp.terminate(nil)
return return
} }
if trimmed == "clear" { if actualCommand == "clear" {
textView.string = "" textView.string = ""
showPrompt() showPrompt()
return return
} }
if trimmed == "help" { if actualCommand == "help" {
showHelp() showHelp()
showPrompt() showPrompt()
return return
} }
if actualCommand == "aliases" {
showAliases()
showPrompt()
return
}
// Add to history // Add to history
if !trimmed.isEmpty { if !trimmed.isEmpty {
HistoryManager.shared.addCommand(trimmed) HistoryManager.shared.addCommand(trimmed)
// Send to shell // Send to shell
if let data = (trimmed + "\n").data(using: .utf8) { if let data = (actualCommand + "\n").data(using: .utf8) {
inputPipe?.fileHandleForWriting.write(data) inputPipe?.fileHandleForWriting.write(data)
} }
} }
@@ -453,12 +475,27 @@ class TerminalSession: NSObject, NSTextViewDelegate, NSMenuDelegate {
Built-in Commands: Built-in Commands:
• clear - Clear screen • clear - Clear screen
• help - Show this help • help - Show this help
• aliases - Show command aliases
• exit - Quit application • 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) { func insertCommand(_ command: String) {
textView.insertText(command, replacementRange: NSRange(location: textView.string.count, length: 0)) textView.insertText(command, replacementRange: NSRange(location: textView.string.count, length: 0))
textView.window?.makeFirstResponder(textView) textView.window?.makeFirstResponder(textView)
@@ -491,6 +528,7 @@ class TabManager {
struct SessionData: Codable { struct SessionData: Codable {
let title: String let title: String
let sessionID: String let sessionID: String
let workingDirectory: String?
} }
func addTab(title: String = "Terminal") -> TerminalSession { func addTab(title: String = "Terminal") -> TerminalSession {
@@ -522,7 +560,7 @@ class TabManager {
} }
func saveSession() { 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) { if let data = try? JSONEncoder().encode(sessionData) {
UserDefaults.standard.set(data, forKey: "terminal_session") UserDefaults.standard.set(data, forKey: "terminal_session")
UserDefaults.standard.set(currentIndex, forKey: "terminal_current_tab") UserDefaults.standard.set(currentIndex, forKey: "terminal_current_tab")