Skip to content

Instantly share code, notes, and snippets.

@hanrw
Created May 24, 2025 04:04
Show Gist options
  • Save hanrw/4fbb048a758b82bdf92288dfdc87dee9 to your computer and use it in GitHub Desktop.
Save hanrw/4fbb048a758b82bdf92288dfdc87dee9 to your computer and use it in GitHub Desktop.
CodeWindowView
import SwiftUI
struct CodeWindowView: View {
@State var visibleLines: [(text: String, lineNumber: Int)] = []
@State private var scrollProxy: ScrollViewProxy?
var body: some View {
ZStack {
// Window chrome
VStack(spacing: 0) {
// Title bar
HStack(spacing: 6) {
Circle()
.fill(Color.red.opacity(0.8))
.frame(width: 12, height: 12)
Circle()
.fill(Color.yellow.opacity(0.8))
.frame(width: 12, height: 12)
Circle()
.fill(Color.green.opacity(0.8))
.frame(width: 12, height: 12)
Spacer()
Text("code.tsx")
.font(.system(size: 11))
.foregroundColor(.secondary)
Spacer()
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Color(NSColor.windowBackgroundColor))
Divider()
// Code area
ScrollViewReader { proxy in
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading, spacing: 0) {
ForEach(Array(visibleLines.enumerated()), id: \.offset) { index, line in
HStack(spacing: 16) {
// Line number with gutter
Text("\(line.lineNumber)")
.font(.system(size: 11, weight: .regular, design: .monospaced))
.foregroundColor(Color(NSColor.tertiaryLabelColor))
.frame(width: 30, alignment: .trailing)
.padding(.trailing, 8)
.background(
Rectangle()
.fill(Color(NSColor.separatorColor).opacity(0.1))
)
// Code line
Text(line.text.isEmpty ? " " : line.text)
.font(.system(size: 12, weight: .regular, design: .monospaced))
.foregroundColor(Color(NSColor.labelColor))
.frame(maxWidth: .infinity, alignment: .leading)
.textSelection(.enabled)
}
.frame(height: 18)
.id(index)
}
}
.padding(.vertical, 8)
}
.frame(width: 600, height: 80)
.background(Color(NSColor.textBackgroundColor))
.onAppear {
scrollProxy = proxy
}
}
}
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color(NSColor.separatorColor), lineWidth: 0.5)
)
.shadow(color: Color.black.opacity(0.1), radius: 4, x: 0, y: 2)
// Gradient overlay
VStack(spacing: 0) {
Spacer()
.frame(height: 36) // Account for title bar
LinearGradient(
gradient: Gradient(stops: [
.init(color: Color(NSColor.textBackgroundColor), location: 0),
.init(color: Color(NSColor.textBackgroundColor).opacity(0.7), location: 0.4),
.init(color: Color(NSColor.textBackgroundColor).opacity(0), location: 1)
]),
startPoint: .top,
endPoint: .bottom
)
.frame(height: 25)
.allowsHitTesting(false)
Spacer()
// Bottom gradient
LinearGradient(
gradient: Gradient(stops: [
.init(color: Color(NSColor.textBackgroundColor).opacity(0), location: 0),
.init(color: Color(NSColor.textBackgroundColor).opacity(0.7), location: 0.6),
.init(color: Color(NSColor.textBackgroundColor), location: 1)
]),
startPoint: .top,
endPoint: .bottom
)
.frame(height: 25)
.allowsHitTesting(false)
}
}
.frame(width: 600, height: 124)
}
}
#Preview {
let codeLines = """
import React from 'react';
import { View, Text } from 'react-native';
import { styles } from './styles';
const MyComponent: React.FC = () => {
return (
<View style={styles.container}>
<Text style={styles.text}>Hello, World!</Text>
</View>
);
};
""".trimmingCharacters(in: .whitespaces)
CodeWindowView(visibleLines: codeLines
.split(separator: "\n")
.enumerated()
.map { (index, line) in
(text: String(line), lineNumber: index + 1)
}
)
}
@hanrw
Copy link
Author

hanrw commented May 24, 2025

Snipaste_2025-05-24_12-04-29

@hanrw
Copy link
Author

hanrw commented May 24, 2025

import SwiftUI
import Combine

// MARK: - View Model for Streaming
class StreamingViewModel: ObservableObject {
    @Published var visibleLines: [(text: String, lineNumber: Int, id: UUID)] = []
    @Published var isAutoScrollEnabled = true
    @Published var shouldScrollToBottom = false
    @Published var scrollChunkSize = 3
    
    private var pendingLines: [(text: String, lineNumber: Int, id: UUID)] = []
    private var updateTimer: Timer?
    
    init() {
        // Start a timer to batch updates
        updateTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
            self.flushPendingLines()
        }
    }
    
    deinit {
        updateTimer?.invalidate()
    }
    
    /// Append a new line to the stream
    func appendLine(_ text: String) {
        let newLineNumber = (visibleLines.last?.lineNumber ?? pendingLines.last?.lineNumber ?? 0) + 1
        pendingLines.append((text: text, lineNumber: newLineNumber, id: UUID()))
        
        // If we have enough lines for a chunk, flush immediately
        if pendingLines.count >= scrollChunkSize {
            flushPendingLines()
        }
    }
    
    /// Append multiple lines to the stream
    func appendLines(_ lines: [String]) {
        let startLineNumber = (visibleLines.last?.lineNumber ?? pendingLines.last?.lineNumber ?? 0) + 1
        let newLines = lines.enumerated().map { index, text in
            (text: text, lineNumber: startLineNumber + index, id: UUID())
        }
        pendingLines.append(contentsOf: newLines)
        
        // Flush in chunks
        while pendingLines.count >= scrollChunkSize {
            flushPendingLines()
        }
    }
    
    /// Flush pending lines in chunks
    private func flushPendingLines() {
        guard !pendingLines.isEmpty else { return }
        
        // Take up to scrollChunkSize lines
        let linesToAdd = Array(pendingLines.prefix(scrollChunkSize))
        pendingLines.removeFirst(min(scrollChunkSize, pendingLines.count))
        
        // Add them all at once
        visibleLines.append(contentsOf: linesToAdd)
        
        if isAutoScrollEnabled {
            // Trigger scroll after the view updates
            DispatchQueue.main.async {
                self.shouldScrollToBottom = true
            }
        }
    }
    
    /// Force flush any remaining lines
    func forceFlush() {
        if !pendingLines.isEmpty {
            visibleLines.append(contentsOf: pendingLines)
            pendingLines.removeAll()
            
            if isAutoScrollEnabled {
                DispatchQueue.main.async {
                    self.shouldScrollToBottom = true
                }
            }
        }
    }
    
    /// Clear all lines
    func clearLines() {
        visibleLines.removeAll()
        pendingLines.removeAll()
    }
    
    /// Enable or disable auto-scrolling
    func setAutoScroll(_ enabled: Bool) {
        isAutoScrollEnabled = enabled
        if enabled {
            forceFlush()
            shouldScrollToBottom = true
        }
    }
    
    /// Set the chunk size for scrolling
    func setScrollChunkSize(_ size: Int) {
        scrollChunkSize = max(1, size)
    }
}

struct CodeWindowView: View {
    @ObservedObject var viewModel: StreamingViewModel
    @State private var scrollViewHeight: CGFloat = 0
    @State private var contentHeight: CGFloat = 0
    @Namespace private var scrollNamespace
    
    init(viewModel: StreamingViewModel = StreamingViewModel()) {
        self.viewModel = viewModel
    }
    
    var body: some View {
        ZStack {
            // Window chrome
            VStack(spacing: 0) {
                // Title bar
                HStack(spacing: 6) {
                    Circle()
                        .fill(Color.red.opacity(0.8))
                        .frame(width: 12, height: 12)
                    Circle()
                        .fill(Color.yellow.opacity(0.8))
                        .frame(width: 12, height: 12)
                    Circle()
                        .fill(Color.green.opacity(0.8))
                        .frame(width: 12, height: 12)
                    
                    Spacer()
                    
                    Text("live-stream.log")
                        .font(.system(size: 11))
                        .foregroundColor(.secondary)
                    
                    Spacer()
                    
                    // Chunk size indicator
                    Text("×\(viewModel.scrollChunkSize)")
                        .font(.system(size: 10, weight: .medium, design: .monospaced))
                        .foregroundColor(.secondary)
                        .padding(.horizontal, 4)
                        .padding(.vertical, 2)
                        .background(
                            RoundedRectangle(cornerRadius: 4)
                                .fill(Color(NSColor.separatorColor).opacity(0.2))
                        )
                    
                    // Auto-scroll toggle button
                    Button(action: {
                        viewModel.setAutoScroll(!viewModel.isAutoScrollEnabled)
                    }) {
                        Image(systemName: viewModel.isAutoScrollEnabled ? "arrow.down.circle.fill" : "arrow.down.circle")
                            .foregroundColor(viewModel.isAutoScrollEnabled ? .accentColor : .secondary)
                            .font(.system(size: 12))
                    }
                    .buttonStyle(PlainButtonStyle())
                    .help("Toggle auto-scroll")
                }
                .padding(.horizontal, 12)
                .padding(.vertical, 8)
                .background(Color(NSColor.windowBackgroundColor))
                
                Divider()
                
                // Code area with custom scroll handling
                GeometryReader { geometry in
                    ScrollViewReader { proxy in
                        ScrollView(.vertical, showsIndicators: true) {
                            VStack(alignment: .leading, spacing: 0) {
                                // Group lines into chunks for visual coherence
                                ForEach(Array(viewModel.visibleLines.enumerated()), id: \.element.id) { index, line in
                                    HStack(spacing: 16) {
                                        // Line number with gutter
                                        Text("\(line.lineNumber)")
                                            .font(.system(size: 11, weight: .regular, design: .monospaced))
                                            .foregroundColor(Color(NSColor.tertiaryLabelColor))
                                            .frame(width: 30, alignment: .trailing)
                                            .padding(.trailing, 8)
                                            .background(
                                                Rectangle()
                                                    .fill(Color(NSColor.separatorColor).opacity(0.1))
                                            )
                                        
                                        // Code line
                                        Text(line.text.isEmpty ? " " : line.text)
                                            .font(.system(size: 12, weight: .regular, design: .monospaced))
                                            .foregroundColor(Color(NSColor.labelColor))
                                            .frame(maxWidth: .infinity, alignment: .leading)
                                            .textSelection(.enabled)
                                    }
                                    .frame(height: 18)
                                    .id(line.id)
                                    // Add a subtle separator every chunk
                                    .overlay(
                                        Group {
                                            if (index + 1) % viewModel.scrollChunkSize == 0 && index < viewModel.visibleLines.count - 1 {
                                                VStack {
                                                    Spacer()
                                                    Divider()
                                                        .opacity(0.1)
                                                }
                                            }
                                        }
                                    )
                                }
                                
                                // Invisible anchor for smooth scrolling
                                Color.clear
                                    .frame(height: 1)
                                    .id("bottom")
                            }
                            .padding(.vertical, 8)
                        }
                        .onChange(of: viewModel.shouldScrollToBottom) { shouldScroll in
                            if shouldScroll {
                                // Smooth animation with spring effect
                                withAnimation(.interpolatingSpring(stiffness: 120, damping: 20)) {
                                    proxy.scrollTo("bottom", anchor: .bottom)
                                }
                                // Reset the flag
                                DispatchQueue.main.async {
                                    viewModel.shouldScrollToBottom = false
                                }
                            }
                        }
                        .onAppear {
                            if viewModel.isAutoScrollEnabled && !viewModel.visibleLines.isEmpty {
                                proxy.scrollTo("bottom", anchor: .bottom)
                            }
                        }
                    }
                }
                .frame(width: 600, height: 80)
                .background(Color(NSColor.textBackgroundColor))
            }
            .cornerRadius(8)
            .overlay(
                RoundedRectangle(cornerRadius: 8)
                    .stroke(Color(NSColor.separatorColor), lineWidth: 0.5)
            )
            .shadow(color: Color.black.opacity(0.1), radius: 4, x: 0, y: 2)
            
            // Gradient overlay (only show when not at bottom)
            if !viewModel.isAutoScrollEnabled {
                VStack(spacing: 0) {
                    Spacer()
                        .frame(height: 36) // Account for title bar
                    
                    LinearGradient(
                        gradient: Gradient(stops: [
                            .init(color: Color(NSColor.textBackgroundColor), location: 0),
                            .init(color: Color(NSColor.textBackgroundColor).opacity(0.7), location: 0.4),
                            .init(color: Color(NSColor.textBackgroundColor).opacity(0), location: 1)
                        ]),
                        startPoint: .top,
                        endPoint: .bottom
                    )
                    .frame(height: 25)
                    .allowsHitTesting(false)
                    
                    Spacer()
                    
                    // Bottom gradient
                    LinearGradient(
                        gradient: Gradient(stops: [
                            .init(color: Color(NSColor.textBackgroundColor).opacity(0), location: 0),
                            .init(color: Color(NSColor.textBackgroundColor).opacity(0.7), location: 0.6),
                            .init(color: Color(NSColor.textBackgroundColor), location: 1)
                        ]),
                        startPoint: .top,
                        endPoint: .bottom
                    )
                    .frame(height: 25)
                    .allowsHitTesting(false)
                }
            }
        }
        .frame(width: 600, height: 124)
    }
}


#Preview {
    struct StreamingDemo: View {
        @StateObject private var streamingModel = StreamingViewModel()
        @State private var timer: Timer?
        @State private var rapidTimer: Timer?
        
        var body: some View {
            VStack(spacing: 20) {
                CodeWindowView(viewModel: streamingModel)
                
                HStack(spacing: 10) {
                    Button("Start Streaming") {
                        startStreaming()
                    }
                    .disabled(timer != nil)
                    
                    Button("Stop Streaming") {
                        stopStreaming()
                    }
                    .disabled(timer == nil)
                    
                    Button("Rapid Stream") {
                        startRapidStreaming()
                    }
                    .disabled(rapidTimer != nil)
                    
                    Button("Stop Rapid") {
                        stopRapidStreaming()
                    }
                    .disabled(rapidTimer == nil)
                    
                    Button("Clear") {
                        streamingModel.clearLines()
                    }
                    
                    Button("Flush") {
                        streamingModel.forceFlush()
                    }
                }
                
                HStack(spacing: 10) {
                    Text("Chunk Size:")
                    ForEach([1, 3, 5], id: \.self) { size in
                        Button("\(size)") {
                            streamingModel.setScrollChunkSize(size)
                        }
                        .buttonStyle(.bordered)
                        .disabled(streamingModel.scrollChunkSize == size)
                    }
                }
            }
            .padding()
        }
        
        private func startStreaming() {
            // Initialize with some sample messages
            let initialMessages = [
                "[INFO] Application started",
                "[DEBUG] Loading configuration...",
                "[INFO] Database connection established",
                "[DEBUG] User authentication enabled",
                "[INFO] Server listening on port 8080",
                "[DEBUG] Cache initialized"
            ]
            
            streamingModel.appendLines(initialMessages)
            
            // Start streaming new messages
            timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
                let messages = [
                    "[INFO] Processing request from user_\(Int.random(in: 1000...9999))",
                    "[DEBUG] Cache hit for key: session_\(UUID().uuidString.prefix(8))",
                    "[WARN] High memory usage detected: \(Int.random(in: 70...95))%",
                    "[INFO] Background task completed successfully",
                    "[ERROR] Network timeout after 30s",
                    "[INFO] Retrying operation...",
                    "[DEBUG] API response time: \(Int.random(in: 50...500))ms"
                ]
                
                let randomMessage = messages.randomElement() ?? "System message"
                let timestamp = Date().formatted(.dateTime.hour().minute().second())
                streamingModel.appendLine("[\(timestamp)] \(randomMessage)")
            }
        }
        
        private func stopStreaming() {
            timer?.invalidate()
            timer = nil
            streamingModel.forceFlush()
        }
        
        private func startRapidStreaming() {
            // Rapid streaming to test smooth scrolling
            rapidTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { _ in
                let timestamp = Date().formatted(.dateTime.hour().minute().second())
                streamingModel.appendLine("[RAPID] Data packet received at \(timestamp)")
            }
        }
        
        private func stopRapidStreaming() {
            rapidTimer?.invalidate()
            rapidTimer = nil
            streamingModel.forceFlush()
        }
    }
    
    return StreamingDemo()
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment