Skip to content

Instantly share code, notes, and snippets.

@timi2506
Created December 26, 2025 13:38
Show Gist options
  • Select an option

  • Save timi2506/3c3ecf66850c02b346e74fad23d999dd to your computer and use it in GitHub Desktop.

Select an option

Save timi2506/3c3ecf66850c02b346e74fad23d999dd to your computer and use it in GitHub Desktop.
YAY
//
// ContentView.swift
// Test
//
// Created by Tim on 21.12.25.
//
import SwiftUI
import Combine
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class ViewModel: ObservableObject {
static let shared = ViewModel()
@Published var elements: [SpeakElement] = []
@Published var selected: [UUID] = []
}
struct ContentView: View {
@StateObject var model = ViewModel.shared
var body: some View {
VStack {
let bindings = model.selected.compactMap { id in
if let index = model.elements.firstIndex(where: { $0.id == id }) {
return $model.elements[index]
} else {
return nil
}
}
if bindings.isEmpty {
Text("No Elements Found")
.padding()
} else {
let columns: [GridItem] = Array(
repeating: GridItem(.flexible(minimum: 350, maximum: 350), spacing: 12),
count: 2
)
ScrollView([.horizontal, .vertical]) {
LazyVGrid(columns: columns, spacing: 10) {
ForEach(bindings) { binding in
SpeakElementView(element: binding)
}
}
}
}
}
.navigationTitle("SpeakIt")
.toolbar {
Button("Add", systemImage: "plus") {
let e = SpeakElement()
model.elements.append(e)
model.selected.append(e.id)
}
}
.frame(minWidth: 500, minHeight: 350)
.animation(.default, value: model.selected)
}
}
struct SpeakElementView: View {
@Binding var element: SpeakElement
@StateObject var model = ViewModel.shared
@State var output = ""
var body: some View {
VStack {
if element.isRunning {
HStack {
if output.isEmpty {
Text("Waiting for Process to start...")
.foregroundStyle(.secondary)
}
ANSITextView(text: output)
Spacer()
}
} else {
TextField("Text To Say", text: $element.text)
}
HStack {
if element.isRunning {
Button("Stop Speaking", systemImage: "stop.fill") {
element.process.terminate()
}
.foregroundStyle(.red)
} else {
Button("Speak Text", systemImage: "play.fill") {
Task {
element.isRunning = true
element.process = Process()
let pipe = Pipe()
element.process.environment = [
"TERM": "xterm-256color",
"CLICOLOR": "1",
"CLICOLOR_FORCE": "1"
]
element.process.standardOutput = pipe
pipe.fileHandleForReading.readabilityHandler = { handle in
let data = handle.availableData
if let line = String(data: data, encoding: .utf8), !line.isEmpty {
DispatchQueue.main.async {
self.output += line
}
}
}
element.process.executableURL = URL(fileURLWithPath: "/usr/bin/say")
element.process.arguments = ["-i", element.text]
try? element.process.run()
await asyncBlocking {
element.process.waitUntilExit()
}
element.isRunning = false
self.output = ""
}
}
.disabled(element.text.isEmpty)
.buttonStyle(.borderedProminent)
}
Spacer()
if element.isRunning {
ProgressView()
.controlSize(.small)
Spacer()
}
Button("Remove", systemImage: "trash") {
model.selected.removeAll { id in
id == element.id
}
model.elements.removeAll { element in
element.id == self.element.id
}
}
}
}
.animation(.default, value: element.isRunning)
.padding()
.background {
RoundedRectangle(cornerRadius: 15)
.foregroundStyle(.ultraThinMaterial)
}
.padding(.vertical, 5)
.padding(.horizontal)
}
func asyncBlocking(_ function: @escaping () -> Void) async {
await withCheckedContinuation { continuation in
DispatchQueue.global(qos: .userInitiated).async {
function()
continuation.resume()
}
}
}
}
struct SpeakElement: Identifiable {
var id = UUID()
var text: String = ""
var isRunning: Bool = false
var process = Process()
}
autoreleasepool {
let app = NSApplication.shared
app.setActivationPolicy(.regular)
TestApp.main()
}
#Preview {
ContentView()
}
struct ANSITextView: View {
let text: String
var body: some View {
Text(parseANSI(text))
.textSelection(.enabled)
.multilineTextAlignment(.leading)
}
private func parseANSI(_ input: String) -> AttributedString {
let lastLineInput: String = {
if let range = input.range(of: "\r", options: .backwards) {
return String(input[range.upperBound...])
}
if let range = input.range(of: "\n", options: .backwards) {
return String(input[range.upperBound...])
}
return input
}()
var result = AttributedString()
var current = AttributedString()
var isHighlighted = false
var i = lastLineInput.startIndex
func flush() {
if !current.characters.isEmpty {
if isHighlighted {
current.backgroundColor = .yellow.opacity(0.5)
} else {
current.backgroundColor = nil
}
result.append(current)
current = AttributedString()
}
}
while i < lastLineInput.endIndex {
if lastLineInput[i] == "\u{1B}" {
flush()
var seqEnd = lastLineInput.index(after: i)
while seqEnd < lastLineInput.endIndex && !lastLineInput[seqEnd].isLetter {
seqEnd = lastLineInput.index(after: seqEnd)
}
if seqEnd < lastLineInput.endIndex {
seqEnd = lastLineInput.index(after: seqEnd)
}
let sequence = String(lastLineInput[i..<seqEnd])
switch sequence {
case "\u{1B}[7m":
isHighlighted = true
case "\u{1B}[m":
isHighlighted = false
default:
break
}
i = seqEnd
} else {
current.append(AttributedString(String(lastLineInput[i])))
i = lastLineInput.index(after: i)
}
}
flush()
return result
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment