Add 5 new features: edit snippets, clear scrollback, category filter, find in terminal, startup commands

1. Edit Snippets: Edit button to modify existing snippets
2. Clear Scrollback: Cmd+Shift+K to clear all terminal output
3. Category Filter: Dropdown to filter snippets by category
4. Find in Terminal: Cmd+F to search terminal output (built-in)
5. Startup Commands: Configure commands to run when opening new tabs

All features tested and working
This commit is contained in:
2026-01-31 23:21:39 +00:00
parent 8573e09563
commit 9561e229aa
6 changed files with 184 additions and 12 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -141,6 +141,7 @@ class MainWindowController: NSWindowController {
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"))
fileMenu.addItem(NSMenuItem(title: "Clear Scrollback", action: #selector(clearScrollback), keyEquivalent: "K"))
// Edit menu
let editMenuItem = NSMenuItem(title: "Edit", action: nil, keyEquivalent: "")
@@ -152,6 +153,8 @@ class MainWindowController: NSWindowController {
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]
@@ -453,6 +456,28 @@ class MainWindowController: NSWindowController {
}
}
@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 clearHistory() {
let alert = NSAlert()
alert.messageText = "Clear Command History?"

View File

@@ -136,6 +136,7 @@ class HistorySearchPanel: NSPanel, NSTableViewDataSource, NSTableViewDelegate, N
class SnippetPanel: NSPanel, NSTableViewDataSource, NSTableViewDelegate, NSTextFieldDelegate {
private var tableView: NSTableView!
private var searchField: NSTextField!
private var categoryPopup: NSPopUpButton!
private var filteredSnippets: [SnippetManager.Snippet] = []
private var completion: ((SnippetManager.Snippet?) -> Void)?
@@ -157,13 +158,24 @@ class SnippetPanel: NSPanel, NSTableViewDataSource, NSTableViewDelegate, NSTextF
container.autoresizingMask = [.width, .height]
// Search field
searchField = NSTextField(frame: NSRect(x: 20, y: container.bounds.height - 42, width: container.bounds.width - 260, height: 24))
searchField = NSTextField(frame: NSRect(x: 20, y: container.bounds.height - 42, width: container.bounds.width - 400, height: 24))
searchField.placeholderString = "Search snippets..."
searchField.autoresizingMask = [.width, .minYMargin]
searchField.delegate = self
// Category filter
let categoryLabel = NSTextField(labelWithString: "Category:")
categoryLabel.frame = NSRect(x: container.bounds.width - 370, y: container.bounds.height - 40, width: 70, height: 20)
categoryLabel.autoresizingMask = [.minXMargin, .minYMargin]
categoryPopup = NSPopUpButton(frame: NSRect(x: container.bounds.width - 295, y: container.bounds.height - 45, width: 120, height: 30))
categoryPopup.autoresizingMask = [.minXMargin, .minYMargin]
categoryPopup.target = self
categoryPopup.action = #selector(categoryChanged)
updateCategoryFilter()
// Add button
let addButton = NSButton(frame: NSRect(x: container.bounds.width - 230, y: container.bounds.height - 45, width: 70, height: 30))
let addButton = NSButton(frame: NSRect(x: container.bounds.width - 165, y: container.bounds.height - 45, width: 50, height: 30))
addButton.title = "Add"
addButton.bezelStyle = .rounded
addButton.target = self
@@ -171,19 +183,21 @@ class SnippetPanel: NSPanel, NSTableViewDataSource, NSTableViewDelegate, NSTextF
addButton.autoresizingMask = [.minXMargin, .minYMargin]
// Export button
let exportButton = NSButton(frame: NSRect(x: container.bounds.width - 155, y: container.bounds.height - 45, width: 70, height: 30))
exportButton.title = "Export"
let exportButton = NSButton(frame: NSRect(x: container.bounds.width - 110, y: container.bounds.height - 45, width: 50, height: 30))
exportButton.title = "↓"
exportButton.bezelStyle = .rounded
exportButton.target = self
exportButton.action = #selector(exportSnippets)
exportButton.toolTip = "Export snippets"
exportButton.autoresizingMask = [.minXMargin, .minYMargin]
// Import button
let importButton = NSButton(frame: NSRect(x: container.bounds.width - 80, y: container.bounds.height - 45, width: 70, height: 30))
importButton.title = "Import"
let importButton = NSButton(frame: NSRect(x: container.bounds.width - 55, y: container.bounds.height - 45, width: 50, height: 30))
importButton.title = "↑"
importButton.bezelStyle = .rounded
importButton.target = self
importButton.action = #selector(importSnippets)
importButton.toolTip = "Import snippets"
importButton.autoresizingMask = [.minXMargin, .minYMargin]
// Table view
@@ -235,14 +249,21 @@ class SnippetPanel: NSPanel, NSTableViewDataSource, NSTableViewDelegate, NSTextF
useButton.action = #selector(selectSnippet)
useButton.autoresizingMask = [.minXMargin, .maxYMargin]
let deleteButton = NSButton(frame: NSRect(x: container.bounds.width - 230, y: 10, width: 100, height: 30))
let editButton = NSButton(frame: NSRect(x: container.bounds.width - 230, y: 10, width: 100, height: 30))
editButton.title = "Edit"
editButton.bezelStyle = .rounded
editButton.target = self
editButton.action = #selector(editSnippet)
editButton.autoresizingMask = [.minXMargin, .maxYMargin]
let deleteButton = NSButton(frame: NSRect(x: container.bounds.width - 340, y: 10, width: 100, height: 30))
deleteButton.title = "Delete"
deleteButton.bezelStyle = .rounded
deleteButton.target = self
deleteButton.action = #selector(deleteSnippet)
deleteButton.autoresizingMask = [.minXMargin, .maxYMargin]
let cancelButton = NSButton(frame: NSRect(x: container.bounds.width - 340, y: 10, width: 100, height: 30))
let cancelButton = NSButton(frame: NSRect(x: container.bounds.width - 450, y: 10, width: 100, height: 30))
cancelButton.title = "Cancel"
cancelButton.bezelStyle = .rounded
cancelButton.target = self
@@ -250,11 +271,14 @@ class SnippetPanel: NSPanel, NSTableViewDataSource, NSTableViewDelegate, NSTextF
cancelButton.autoresizingMask = [.minXMargin, .maxYMargin]
container.addSubview(searchField)
container.addSubview(categoryLabel)
container.addSubview(categoryPopup)
container.addSubview(addButton)
container.addSubview(exportButton)
container.addSubview(importButton)
container.addSubview(scrollView)
container.addSubview(useButton)
container.addSubview(editButton)
container.addSubview(deleteButton)
container.addSubview(cancelButton)
@@ -272,13 +296,48 @@ class SnippetPanel: NSPanel, NSTableViewDataSource, NSTableViewDelegate, NSTextF
}
}
func controlTextDidChange(_ obj: Notification) {
private func updateCategoryFilter() {
categoryPopup.removeAllItems()
categoryPopup.addItem(withTitle: "All Categories")
let categories = Set(SnippetManager.shared.getAllSnippets().map { $0.category }).sorted()
categoryPopup.addItems(withTitles: categories)
}
@objc private func categoryChanged() {
filterSnippets()
}
private func filterSnippets() {
let query = searchField.stringValue
filteredSnippets = SnippetManager.shared.searchSnippets(query)
let selectedCategory = categoryPopup.titleOfSelectedItem ?? "All Categories"
var snippets = SnippetManager.shared.getAllSnippets()
// Filter by category
if selectedCategory != "All Categories" {
snippets = snippets.filter { $0.category == selectedCategory }
}
// Filter by search query
if !query.isEmpty {
snippets = snippets.filter {
$0.name.localizedCaseInsensitiveContains(query) ||
$0.command.localizedCaseInsensitiveContains(query) ||
$0.category.localizedCaseInsensitiveContains(query) ||
$0.tags.contains { $0.localizedCaseInsensitiveContains(query) }
}
}
filteredSnippets = snippets
tableView.reloadData()
updateEmptyState()
}
func controlTextDidChange(_ obj: Notification) {
filterSnippets()
}
@objc private func addSnippet() {
let alert = NSAlert()
alert.messageText = "Add New Snippet"
@@ -317,6 +376,7 @@ class SnippetPanel: NSPanel, NSTableViewDataSource, NSTableViewDelegate, NSTextF
filteredSnippets = SnippetManager.shared.getAllSnippets()
tableView.reloadData()
updateEmptyState()
updateCategoryFilter()
}
}
@@ -327,6 +387,62 @@ class SnippetPanel: NSPanel, NSTableViewDataSource, NSTableViewDelegate, NSTextF
close()
}
@objc private func editSnippet() {
let row = tableView.selectedRow
guard row >= 0 && row < filteredSnippets.count else { return }
let snippet = filteredSnippets[row]
let alert = NSAlert()
alert.messageText = "Edit Snippet"
alert.informativeText = "Update snippet details:"
let nameField = NSTextField(frame: NSRect(x: 0, y: 90, width: 300, height: 24))
nameField.stringValue = snippet.name
nameField.placeholderString = "Name"
let commandField = NSTextField(frame: NSRect(x: 0, y: 60, width: 300, height: 24))
commandField.stringValue = snippet.command
commandField.placeholderString = "Command"
let categoryField = NSTextField(frame: NSRect(x: 0, y: 30, width: 300, height: 24))
categoryField.stringValue = snippet.category
categoryField.placeholderString = "Category"
let descField = NSTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 24))
descField.stringValue = snippet.description
descField.placeholderString = "Description"
let view = NSView(frame: NSRect(x: 0, y: 0, width: 300, height: 120))
view.addSubview(nameField)
view.addSubview(commandField)
view.addSubview(categoryField)
view.addSubview(descField)
alert.accessoryView = view
alert.addButton(withTitle: "Save")
alert.addButton(withTitle: "Cancel")
if alert.runModal() == .alertFirstButtonReturn {
// Remove old snippet
SnippetManager.shared.removeSnippet(snippet.id)
// Add updated snippet
let updatedSnippet = SnippetManager.Snippet(
name: nameField.stringValue.isEmpty ? snippet.name : nameField.stringValue,
command: commandField.stringValue.isEmpty ? snippet.command : commandField.stringValue,
description: descField.stringValue,
category: categoryField.stringValue.isEmpty ? snippet.category : categoryField.stringValue
)
SnippetManager.shared.addSnippet(updatedSnippet)
filteredSnippets = SnippetManager.shared.getAllSnippets()
tableView.reloadData()
updateEmptyState()
updateCategoryFilter()
}
}
@objc private func deleteSnippet() {
let row = tableView.selectedRow
guard row >= 0 && row < filteredSnippets.count else { return }
@@ -344,6 +460,7 @@ class SnippetPanel: NSPanel, NSTableViewDataSource, NSTableViewDelegate, NSTextF
filteredSnippets = SnippetManager.shared.getAllSnippets()
tableView.reloadData()
updateEmptyState()
updateCategoryFilter()
}
}
@@ -409,6 +526,7 @@ class SnippetPanel: NSPanel, NSTableViewDataSource, NSTableViewDelegate, NSTextF
filteredSnippets = SnippetManager.shared.getAllSnippets()
tableView.reloadData()
updateEmptyState()
updateCategoryFilter()
let success = NSAlert()
success.messageText = "Import Successful"
@@ -441,6 +559,7 @@ class SnippetPanel: NSPanel, NSTableViewDataSource, NSTableViewDelegate, NSTextF
self.completion = completion
center()
searchField.stringValue = ""
updateCategoryFilter()
filteredSnippets = SnippetManager.shared.getAllSnippets()
tableView.reloadData()
updateEmptyState()
@@ -650,7 +769,7 @@ class AIGeneratorPanel: NSPanel, NSTextFieldDelegate {
class SettingsWindowController: NSWindowController {
convenience init() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 500, height: 450),
contentRect: NSRect(x: 0, y: 0, width: 500, height: 550),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
@@ -773,8 +892,23 @@ class SettingsWindowController: NSWindowController {
yPos -= 50
// Startup commands
let startupLabel = NSTextField(labelWithString: "Startup Commands:")
startupLabel.frame = NSRect(x: 20, y: yPos, width: 120, height: 20)
let startupField = NSTextField(frame: NSRect(x: 150, y: yPos, width: 320, height: 24))
startupField.placeholderString = "Commands to run on new tab (comma separated)"
startupField.stringValue = TerminalSettings.shared.startupCommands.joined(separator: ", ")
startupField.target = self
startupField.action = #selector(startupCommandsChanged(_:))
container.addSubview(startupLabel)
container.addSubview(startupField)
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")
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.")
infoLabel.frame = NSRect(x: 20, y: 20, width: 460, height: 40)
infoLabel.textColor = .secondaryLabelColor
infoLabel.font = NSFont.systemFont(ofSize: 11)
@@ -825,4 +959,9 @@ class SettingsWindowController: NSWindowController {
TerminalSettings.shared.ollamaModel = sender.stringValue
AIManager.shared.updateService()
}
@objc private func startupCommandsChanged(_ sender: NSTextField) {
let commands = sender.stringValue.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }
TerminalSettings.shared.startupCommands = commands.filter { !$0.isEmpty }
}
}

View File

@@ -15,6 +15,7 @@ class TerminalSettings {
var ollamaModel: String = "llama3.2:latest"
var enableAutoSuggestions: Bool = true
var maxHistorySize: Int = 1000
var startupCommands: [String] = []
enum AIProvider: String, CaseIterable {
case none = "None"

View File

@@ -179,6 +179,13 @@ class TerminalSession: NSObject, NSTextViewDelegate, NSMenuDelegate {
// Wait a bit for shell to initialize, then show prompt
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
self?.showPrompt()
// Run startup commands
for command in TerminalSettings.shared.startupCommands {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self?.executeCommand(command)
}
}
}
} catch {
appendText("Error starting shell: \(error)\n")