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