170 lines
5.6 KiB
Swift
170 lines
5.6 KiB
Swift
import Cocoa
|
|
|
|
class TerminalViewController: NSViewController, NSTextViewDelegate {
|
|
var textView: NSTextView!
|
|
var shellTask: Process?
|
|
var inputPipe: Pipe?
|
|
var outputPipe: Pipe?
|
|
var promptPosition = 0
|
|
|
|
override func loadView() {
|
|
let scrollView = NSScrollView(frame: NSRect(x: 0, y: 0, width: 800, height: 600))
|
|
scrollView.autoresizingMask = [.width, .height]
|
|
scrollView.hasVerticalScroller = true
|
|
|
|
textView = NSTextView(frame: scrollView.bounds)
|
|
textView.autoresizingMask = [.width, .height]
|
|
textView.backgroundColor = .black
|
|
textView.textColor = .green
|
|
textView.font = NSFont.monospacedSystemFont(ofSize: 14, weight: .regular)
|
|
textView.isEditable = true
|
|
textView.delegate = self
|
|
|
|
scrollView.documentView = textView
|
|
self.view = scrollView
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
appendText("Simple Terminal v1.0\n")
|
|
appendText("Type commands and press Enter\n")
|
|
appendText("Type 'exit' to quit\n\n")
|
|
startShell()
|
|
}
|
|
|
|
func startShell() {
|
|
shellTask = Process()
|
|
inputPipe = Pipe()
|
|
outputPipe = Pipe()
|
|
|
|
shellTask?.executableURL = URL(fileURLWithPath: "/bin/zsh")
|
|
shellTask?.arguments = ["-i"]
|
|
shellTask?.environment = [
|
|
"TERM": "dumb",
|
|
"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()
|
|
} catch {
|
|
appendText("Error starting shell: \(error)\n")
|
|
}
|
|
|
|
showPrompt()
|
|
}
|
|
|
|
func showPrompt() {
|
|
appendText("$ ")
|
|
promptPosition = textView.string.count
|
|
}
|
|
|
|
func appendText(_ text: String) {
|
|
// Strip ANSI escape codes for simpler display
|
|
let cleaned = 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)
|
|
|
|
let attr = NSAttributedString(string: cleaned, attributes: [
|
|
.foregroundColor: NSColor.green,
|
|
.font: NSFont.monospacedSystemFont(ofSize: 14, weight: .regular)
|
|
])
|
|
textView.textStorage?.append(attr)
|
|
textView.scrollToEndOfDocument(nil)
|
|
}
|
|
|
|
func textView(_ textView: NSTextView, shouldChangeTextIn range: NSRange, replacementString: String?) -> Bool {
|
|
// Don't allow editing before prompt
|
|
if range.location < promptPosition {
|
|
return false
|
|
}
|
|
|
|
// Handle Enter key
|
|
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.isEmpty {
|
|
if let data = (trimmed + "\n").data(using: .utf8) {
|
|
inputPipe?.fileHandleForWriting.write(data)
|
|
}
|
|
}
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
|
self?.showPrompt()
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
shellTask?.terminate()
|
|
}
|
|
}
|
|
|
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
|
var window: NSWindow!
|
|
|
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
window = NSWindow(
|
|
contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
|
|
styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
|
backing: .buffered,
|
|
defer: false
|
|
)
|
|
window.center()
|
|
window.title = "Simple Terminal"
|
|
|
|
let viewController = TerminalViewController()
|
|
window.contentViewController = viewController
|
|
|
|
window.makeKeyAndOrderFront(nil)
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
}
|
|
|
|
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
|
return true
|
|
}
|
|
}
|
|
|
|
let app = NSApplication.shared
|
|
let delegate = AppDelegate()
|
|
app.delegate = delegate
|
|
app.run()
|