Initial Push
This commit is contained in:
344
EnhancedTerminal.swift
Normal file
344
EnhancedTerminal.swift
Normal file
@@ -0,0 +1,344 @@
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
// MARK: - Configuration & Settings
|
||||
class TerminalSettings {
|
||||
static let shared = TerminalSettings()
|
||||
|
||||
var theme: Theme = .dracula
|
||||
var fontSize: CGFloat = 14
|
||||
var fontName: String = "Menlo"
|
||||
var opacity: CGFloat = 1.0
|
||||
var aiProvider: AIProvider = .none
|
||||
var openAIKey: String = ""
|
||||
var ollamaURL: String = "http://localhost:11434"
|
||||
var ollamaModel: String = "llama2"
|
||||
|
||||
enum AIProvider: String {
|
||||
case none = "None"
|
||||
case openai = "OpenAI"
|
||||
case ollama = "Ollama"
|
||||
}
|
||||
|
||||
struct Theme {
|
||||
let name: String
|
||||
let background: NSColor
|
||||
let foreground: NSColor
|
||||
let cursor: NSColor
|
||||
let selection: NSColor
|
||||
|
||||
static let dracula = Theme(
|
||||
name: "Dracula",
|
||||
background: NSColor(red: 0.16, green: 0.16, blue: 0.21, alpha: 1.0),
|
||||
foreground: NSColor(red: 0.95, green: 0.95, blue: 0.95, alpha: 1.0),
|
||||
cursor: NSColor(red: 0.97, green: 0.97, blue: 0.95, alpha: 1.0),
|
||||
selection: NSColor(red: 0.27, green: 0.28, blue: 0.35, alpha: 1.0)
|
||||
)
|
||||
|
||||
static let solarizedDark = Theme(
|
||||
name: "Solarized Dark",
|
||||
background: NSColor(red: 0.0, green: 0.17, blue: 0.21, alpha: 1.0),
|
||||
foreground: NSColor(red: 0.51, green: 0.58, blue: 0.59, alpha: 1.0),
|
||||
cursor: NSColor(red: 0.51, green: 0.58, blue: 0.59, alpha: 1.0),
|
||||
selection: NSColor(red: 0.03, green: 0.21, blue: 0.26, alpha: 1.0)
|
||||
)
|
||||
|
||||
static let nord = Theme(
|
||||
name: "Nord",
|
||||
background: NSColor(red: 0.18, green: 0.20, blue: 0.25, alpha: 1.0),
|
||||
foreground: NSColor(red: 0.85, green: 0.87, blue: 0.91, alpha: 1.0),
|
||||
cursor: NSColor(red: 0.85, green: 0.87, blue: 0.91, alpha: 1.0),
|
||||
selection: NSColor(red: 0.26, green: 0.30, blue: 0.37, alpha: 1.0)
|
||||
)
|
||||
|
||||
static let classic = Theme(
|
||||
name: "Classic",
|
||||
background: .black,
|
||||
foreground: NSColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0),
|
||||
cursor: NSColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0),
|
||||
selection: NSColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1.0)
|
||||
)
|
||||
|
||||
static let allThemes: [Theme] = [.classic, .dracula, .solarizedDark, .nord]
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Command Snippet Manager
|
||||
class SnippetManager {
|
||||
static let shared = SnippetManager()
|
||||
private var snippets: [Snippet] = []
|
||||
|
||||
struct Snippet: Codable {
|
||||
let id: UUID
|
||||
var name: String
|
||||
var command: String
|
||||
var description: String
|
||||
var tags: [String]
|
||||
var category: String
|
||||
|
||||
init(name: String, command: String, description: String = "", tags: [String] = [], category: String = "General") {
|
||||
self.id = UUID()
|
||||
self.name = name
|
||||
self.command = command
|
||||
self.description = description
|
||||
self.tags = tags
|
||||
self.category = category
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
loadSnippets()
|
||||
if snippets.isEmpty {
|
||||
addDefaultSnippets()
|
||||
}
|
||||
}
|
||||
|
||||
func addSnippet(_ snippet: Snippet) {
|
||||
snippets.append(snippet)
|
||||
saveSnippets()
|
||||
}
|
||||
|
||||
func removeSnippet(_ id: UUID) {
|
||||
snippets.removeAll { $0.id == id }
|
||||
saveSnippets()
|
||||
}
|
||||
|
||||
func searchSnippets(_ query: String) -> [Snippet] {
|
||||
if query.isEmpty { return snippets }
|
||||
return snippets.filter {
|
||||
$0.name.localizedCaseInsensitiveContains(query) ||
|
||||
$0.command.localizedCaseInsensitiveContains(query) ||
|
||||
$0.tags.contains { $0.localizedCaseInsensitiveContains(query) }
|
||||
}
|
||||
}
|
||||
|
||||
private func addDefaultSnippets() {
|
||||
snippets = [
|
||||
Snippet(name: "List files", command: "ls -lah", description: "List all files with details", tags: ["files", "list"], category: "Files"),
|
||||
Snippet(name: "Find large files", command: "find . -type f -size +100M", description: "Find files larger than 100MB", tags: ["find", "files"], category: "Files"),
|
||||
Snippet(name: "Git status", command: "git status", description: "Show git repository status", tags: ["git"], category: "Git"),
|
||||
Snippet(name: "Git commit", command: "git add . && git commit -m \"\"", description: "Stage and commit changes", tags: ["git"], category: "Git"),
|
||||
Snippet(name: "Docker ps", command: "docker ps -a", description: "List all containers", tags: ["docker"], category: "Docker"),
|
||||
Snippet(name: "Kill port", command: "lsof -ti:8080 | xargs kill -9", description: "Kill process on port 8080", tags: ["network", "kill"], category: "Network")
|
||||
]
|
||||
saveSnippets()
|
||||
}
|
||||
|
||||
private func saveSnippets() {
|
||||
if let data = try? JSONEncoder().encode(snippets) {
|
||||
UserDefaults.standard.set(data, forKey: "terminal_snippets")
|
||||
}
|
||||
}
|
||||
|
||||
private func loadSnippets() {
|
||||
if let data = UserDefaults.standard.data(forKey: "terminal_snippets"),
|
||||
let loaded = try? JSONDecoder().decode([Snippet].self, from: data) {
|
||||
snippets = loaded
|
||||
}
|
||||
}
|
||||
|
||||
func getAllSnippets() -> [Snippet] {
|
||||
return snippets
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Command History Manager
|
||||
class HistoryManager {
|
||||
static let shared = HistoryManager()
|
||||
private var history: [String] = []
|
||||
private let maxHistory = 1000
|
||||
|
||||
init() {
|
||||
loadHistory()
|
||||
}
|
||||
|
||||
func addCommand(_ command: String) {
|
||||
guard !command.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
|
||||
history.append(command)
|
||||
if history.count > maxHistory {
|
||||
history.removeFirst()
|
||||
}
|
||||
saveHistory()
|
||||
}
|
||||
|
||||
func searchHistory(_ query: String) -> [String] {
|
||||
if query.isEmpty { return Array(history.reversed().prefix(50)) }
|
||||
return history.reversed().filter { $0.localizedCaseInsensitiveContains(query) }
|
||||
}
|
||||
|
||||
func getRecentCommands(_ count: Int = 10) -> [String] {
|
||||
return Array(history.reversed().prefix(count))
|
||||
}
|
||||
|
||||
private func saveHistory() {
|
||||
UserDefaults.standard.set(history, forKey: "terminal_history")
|
||||
}
|
||||
|
||||
private func loadHistory() {
|
||||
history = UserDefaults.standard.stringArray(forKey: "terminal_history") ?? []
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AI Service
|
||||
protocol AIService {
|
||||
func generateCommand(from prompt: String, completion: @escaping (Result<String, Error>) -> Void)
|
||||
func explainError(_ error: String, completion: @escaping (Result<String, Error>) -> Void)
|
||||
func suggestNextCommand(history: [String], completion: @escaping (Result<String, Error>) -> Void)
|
||||
}
|
||||
|
||||
class OpenAIService: AIService {
|
||||
private let apiKey: String
|
||||
|
||||
init(apiKey: String) {
|
||||
self.apiKey = apiKey
|
||||
}
|
||||
|
||||
func generateCommand(from prompt: String, completion: @escaping (Result<String, Error>) -> Void) {
|
||||
let systemPrompt = "You are a terminal command expert. Generate only the command, no explanations. Respond with just the command."
|
||||
makeRequest(system: systemPrompt, user: prompt, completion: completion)
|
||||
}
|
||||
|
||||
func explainError(_ error: String, completion: @escaping (Result<String, Error>) -> Void) {
|
||||
let systemPrompt = "You are a terminal expert. Explain this error briefly and suggest a fix."
|
||||
makeRequest(system: systemPrompt, user: error, completion: completion)
|
||||
}
|
||||
|
||||
func suggestNextCommand(history: [String], completion: @escaping (Result<String, Error>) -> Void) {
|
||||
let systemPrompt = "Based on command history, suggest the next logical command. Respond with just the command."
|
||||
let historyText = history.suffix(5).joined(separator: "\n")
|
||||
makeRequest(system: systemPrompt, user: "Recent commands:\n\(historyText)", completion: completion)
|
||||
}
|
||||
|
||||
private func makeRequest(system: String, user: String, completion: @escaping (Result<String, Error>) -> Void) {
|
||||
guard let url = URL(string: "https://api.openai.com/v1/chat/completions") else { return }
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let body: [String: Any] = [
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
["role": "system", "content": system],
|
||||
["role": "user", "content": user]
|
||||
],
|
||||
"max_tokens": 150,
|
||||
"temperature": 0.7
|
||||
]
|
||||
|
||||
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
|
||||
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data,
|
||||
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let choices = json["choices"] as? [[String: Any]],
|
||||
let firstChoice = choices.first,
|
||||
let message = firstChoice["message"] as? [String: Any],
|
||||
let content = message["content"] as? String else {
|
||||
completion(.failure(NSError(domain: "OpenAI", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid response"])))
|
||||
return
|
||||
}
|
||||
|
||||
completion(.success(content.trimmingCharacters(in: .whitespacesAndNewlines)))
|
||||
}.resume()
|
||||
}
|
||||
}
|
||||
|
||||
class OllamaService: AIService {
|
||||
private let baseURL: String
|
||||
private let model: String
|
||||
|
||||
init(baseURL: String, model: String) {
|
||||
self.baseURL = baseURL
|
||||
self.model = model
|
||||
}
|
||||
|
||||
func generateCommand(from prompt: String, completion: @escaping (Result<String, Error>) -> Void) {
|
||||
let fullPrompt = "Generate only the terminal command for: \(prompt). Respond with just the command, no explanation."
|
||||
makeRequest(prompt: fullPrompt, completion: completion)
|
||||
}
|
||||
|
||||
func explainError(_ error: String, completion: @escaping (Result<String, Error>) -> Void) {
|
||||
let fullPrompt = "Explain this terminal error briefly and suggest a fix:\n\(error)"
|
||||
makeRequest(prompt: fullPrompt, completion: completion)
|
||||
}
|
||||
|
||||
func suggestNextCommand(history: [String], completion: @escaping (Result<String, Error>) -> Void) {
|
||||
let historyText = history.suffix(5).joined(separator: "\n")
|
||||
let fullPrompt = "Based on these recent commands, suggest the next logical command:\n\(historyText)\nRespond with just the command."
|
||||
makeRequest(prompt: fullPrompt, completion: completion)
|
||||
}
|
||||
|
||||
private func makeRequest(prompt: String, completion: @escaping (Result<String, Error>) -> Void) {
|
||||
guard let url = URL(string: "\(baseURL)/api/generate") else { return }
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let body: [String: Any] = [
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"stream": false
|
||||
]
|
||||
|
||||
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
|
||||
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data,
|
||||
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let response = json["response"] as? String else {
|
||||
completion(.failure(NSError(domain: "Ollama", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid response"])))
|
||||
return
|
||||
}
|
||||
|
||||
completion(.success(response.trimmingCharacters(in: .whitespacesAndNewlines)))
|
||||
}.resume()
|
||||
}
|
||||
}
|
||||
|
||||
class AIManager {
|
||||
static let shared = AIManager()
|
||||
private var service: AIService?
|
||||
|
||||
func updateService() {
|
||||
let settings = TerminalSettings.shared
|
||||
switch settings.aiProvider {
|
||||
case .openai:
|
||||
if !settings.openAIKey.isEmpty {
|
||||
service = OpenAIService(apiKey: settings.openAIKey)
|
||||
}
|
||||
case .ollama:
|
||||
service = OllamaService(baseURL: settings.ollamaURL, model: settings.ollamaModel)
|
||||
case .none:
|
||||
service = nil
|
||||
}
|
||||
}
|
||||
|
||||
func isAvailable() -> Bool {
|
||||
return service != nil
|
||||
}
|
||||
|
||||
func generateCommand(from prompt: String, completion: @escaping (Result<String, Error>) -> Void) {
|
||||
service?.generateCommand(from: prompt, completion: completion)
|
||||
}
|
||||
|
||||
func explainError(_ error: String, completion: @escaping (Result<String, Error>) -> Void) {
|
||||
service?.explainError(error, completion: completion)
|
||||
}
|
||||
|
||||
func suggestNextCommand(history: [String], completion: @escaping (Result<String, Error>) -> Void) {
|
||||
service?.suggestNextCommand(history: history, completion: completion)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user