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