Initial Push
This commit is contained in:
213
TerminalViewController.swift
Normal file
213
TerminalViewController.swift
Normal file
@@ -0,0 +1,213 @@
|
||||
import Cocoa
|
||||
|
||||
// MARK: - Terminal Session
|
||||
class TerminalSession: NSObject, NSTextViewDelegate {
|
||||
var textView: NSTextView!
|
||||
var shellTask: Process?
|
||||
var inputPipe: Pipe?
|
||||
var outputPipe: Pipe?
|
||||
var promptPosition = 0
|
||||
var sessionID: UUID
|
||||
var title: String
|
||||
private var outputBuffer: String = ""
|
||||
|
||||
init(title: String = "Terminal") {
|
||||
self.sessionID = UUID()
|
||||
self.title = title
|
||||
super.init()
|
||||
}
|
||||
|
||||
func setupTextView(frame: NSRect) {
|
||||
let scrollView = NSScrollView(frame: frame)
|
||||
scrollView.autoresizingMask = [.width, .height]
|
||||
scrollView.hasVerticalScroller = true
|
||||
|
||||
textView = NSTextView(frame: scrollView.bounds)
|
||||
textView.autoresizingMask = [.width, .height]
|
||||
applyTheme()
|
||||
textView.isEditable = true
|
||||
textView.delegate = self
|
||||
textView.font = NSFont(name: TerminalSettings.shared.fontName, size: TerminalSettings.shared.fontSize)
|
||||
|
||||
scrollView.documentView = textView
|
||||
}
|
||||
|
||||
func applyTheme() {
|
||||
let theme = TerminalSettings.shared.theme
|
||||
textView?.backgroundColor = theme.background
|
||||
textView?.textColor = theme.foreground
|
||||
textView?.insertionPointColor = theme.cursor
|
||||
textView?.selectedTextAttributes = [
|
||||
.backgroundColor: theme.selection
|
||||
]
|
||||
}
|
||||
|
||||
func startShell() {
|
||||
shellTask = Process()
|
||||
inputPipe = Pipe()
|
||||
outputPipe = Pipe()
|
||||
|
||||
shellTask?.executableURL = URL(fileURLWithPath: "/bin/zsh")
|
||||
shellTask?.arguments = ["-i"]
|
||||
shellTask?.environment = [
|
||||
"TERM": "xterm-256color",
|
||||
"PS1": "$ ",
|
||||
"PATH": ProcessInfo.processInfo.environment["PATH"] ?? "/usr/bin:/bin:/usr/sbin:/sbin",
|
||||
"HOME": ProcessInfo.processInfo.environment["HOME"] ?? ""
|
||||
]
|
||||
shellTask?.standardInput = inputPipe
|
||||
shellTask?.standardOutput = outputPipe
|
||||
shellTask?.standardError = outputPipe
|
||||
|
||||
outputPipe?.fileHandleForReading.readabilityHandler = { [weak self] handle in
|
||||
let data = handle.availableData
|
||||
if data.count > 0, let output = String(data: data, encoding: .utf8) {
|
||||
DispatchQueue.main.async {
|
||||
self?.appendText(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
try shellTask?.run()
|
||||
appendText("Enhanced Terminal v2.0\n")
|
||||
appendText("Type 'help' for commands, Cmd+K for AI, Cmd+P for snippets\n\n")
|
||||
showPrompt()
|
||||
} catch {
|
||||
appendText("Error starting shell: \(error)\n")
|
||||
}
|
||||
}
|
||||
|
||||
func showPrompt() {
|
||||
appendText("$ ")
|
||||
promptPosition = textView.string.count
|
||||
}
|
||||
|
||||
func appendText(_ text: String) {
|
||||
let cleaned = cleanANSI(text)
|
||||
let theme = TerminalSettings.shared.theme
|
||||
let attr = NSAttributedString(string: cleaned, attributes: [
|
||||
.foregroundColor: theme.foreground,
|
||||
.font: NSFont(name: TerminalSettings.shared.fontName, size: TerminalSettings.shared.fontSize) ?? NSFont.monospacedSystemFont(ofSize: TerminalSettings.shared.fontSize, weight: .regular)
|
||||
])
|
||||
textView.textStorage?.append(attr)
|
||||
textView.scrollToEndOfDocument(nil)
|
||||
}
|
||||
|
||||
private func cleanANSI(_ text: String) -> String {
|
||||
return text
|
||||
.replacingOccurrences(of: "\u{1B}\\[[0-9;]*[a-zA-Z]", with: "", options: .regularExpression)
|
||||
.replacingOccurrences(of: "\u{1B}\\][0-9];[^\u{07}]*\u{07}", with: "", options: .regularExpression)
|
||||
.replacingOccurrences(of: "\\]633;[^\u{07}]*\u{07}", with: "", options: .regularExpression)
|
||||
.replacingOccurrences(of: "\\]697;[^\u{07}]*\u{07}", with: "", options: .regularExpression)
|
||||
.replacingOccurrences(of: "\\]1;[^\u{07}]*\u{07}", with: "", options: .regularExpression)
|
||||
.replacingOccurrences(of: "\\]2;[^\u{07}]*\u{07}", with: "", options: .regularExpression)
|
||||
.replacingOccurrences(of: "\\]7;[^\u{07}]*\\\\", with: "", options: .regularExpression)
|
||||
}
|
||||
|
||||
func textView(_ textView: NSTextView, shouldChangeTextIn range: NSRange, replacementString: String?) -> Bool {
|
||||
if range.location < promptPosition {
|
||||
return false
|
||||
}
|
||||
|
||||
if replacementString == "\n" {
|
||||
let command = String(textView.string.suffix(from: textView.string.index(textView.string.startIndex, offsetBy: promptPosition)))
|
||||
executeCommand(command)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func executeCommand(_ command: String) {
|
||||
appendText("\n")
|
||||
|
||||
let trimmed = command.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
if trimmed == "exit" {
|
||||
NSApp.terminate(nil)
|
||||
return
|
||||
}
|
||||
|
||||
if trimmed == "clear" {
|
||||
textView.string = ""
|
||||
showPrompt()
|
||||
return
|
||||
}
|
||||
|
||||
if trimmed == "help" {
|
||||
showHelp()
|
||||
showPrompt()
|
||||
return
|
||||
}
|
||||
|
||||
if !trimmed.isEmpty {
|
||||
HistoryManager.shared.addCommand(trimmed)
|
||||
if let data = (trimmed + "\n").data(using: .utf8) {
|
||||
inputPipe?.fileHandleForWriting.write(data)
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
||||
self?.showPrompt()
|
||||
}
|
||||
}
|
||||
|
||||
private func showHelp() {
|
||||
appendText("""
|
||||
Enhanced Terminal Commands:
|
||||
- Cmd+T: New tab
|
||||
- Cmd+W: Close tab
|
||||
- Cmd+D: Split pane vertically
|
||||
- Cmd+Shift+D: Split pane horizontally
|
||||
- Cmd+K: AI command generation
|
||||
- Cmd+P: Open snippets
|
||||
- Cmd+R: Search history
|
||||
- Cmd+,: Settings
|
||||
- clear: Clear screen
|
||||
- exit: Quit application
|
||||
|
||||
""")
|
||||
}
|
||||
|
||||
func cleanup() {
|
||||
shellTask?.terminate()
|
||||
outputPipe?.fileHandleForReading.readabilityHandler = nil
|
||||
}
|
||||
|
||||
deinit {
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Tab Manager
|
||||
class TabManager {
|
||||
var tabs: [TerminalSession] = []
|
||||
var currentIndex: Int = 0
|
||||
|
||||
func addTab(title: String = "Terminal") -> TerminalSession {
|
||||
let session = TerminalSession(title: title)
|
||||
tabs.append(session)
|
||||
currentIndex = tabs.count - 1
|
||||
return session
|
||||
}
|
||||
|
||||
func removeTab(at index: Int) {
|
||||
guard index < tabs.count else { return }
|
||||
tabs[index].cleanup()
|
||||
tabs.remove(at: index)
|
||||
if currentIndex >= tabs.count {
|
||||
currentIndex = max(0, tabs.count - 1)
|
||||
}
|
||||
}
|
||||
|
||||
func selectTab(at index: Int) {
|
||||
guard index < tabs.count else { return }
|
||||
currentIndex = index
|
||||
}
|
||||
|
||||
func getCurrentSession() -> TerminalSession? {
|
||||
guard currentIndex < tabs.count else { return nil }
|
||||
return tabs[currentIndex]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user