resopt-cli 0.7.0

Find, review and safely apply resource optimizations for Apple and Android projects.
Documentation
import AppKit
import SwiftUI
import UniformTypeIdentifiers
import WebKit

@main
struct ResoptApp: App {
    @StateObject private var model = ReviewSession()

    var body: some Scene {
        Window("resopt", id: "main") {
            ContentView(model: model)
                .frame(minWidth: 900, minHeight: 640)
                .onDisappear { model.stop() }
        }
        .windowStyle(.titleBar)
    }
}

private struct ContentView: View {
    @ObservedObject var model: ReviewSession
    @State private var choosingProject = false
    @State private var choosingReport = false
    @State private var jobs = ProcessInfo.processInfo.activeProcessorCount >= 4 ? 4 : 2

    var body: some View {
        VStack(spacing: 0) {
            HStack(spacing: 12) {
                Image(systemName: "photo.stack")
                    .foregroundStyle(.tint)
                Text("resopt")
                    .font(.headline)
                Button(model.project == nil ? "选择项目目录" : "更换项目目录") {
                    choosingProject = true
                }
                .disabled(model.isStarting)
                Button("打开已有报告") { choosingReport = true }
                    .disabled(model.isStarting)
                Picker("并行任务", selection: $jobs) {
                    Text("2").tag(2)
                    Text("4").tag(4)
                    Text("8").tag(8)
                }
                .frame(width: 110)
                .help("同时分析的不同图片数量。任务数越多,占用的内存也越多;下次扫描时生效。")
                if let project = model.project {
                    Text(project.path)
                        .lineLimit(1)
                        .truncationMode(.middle)
                        .foregroundStyle(.secondary)
                        .help(project.path)
                }
                Spacer(minLength: 8)
                if let report = model.reportDirectory {
                    Button("显示报告目录") {
                        NSWorkspace.shared.activateFileViewerSelecting([report])
                    }
                }
            }
            .padding(.horizontal, 16)
            .padding(.vertical, 10)

            Divider()

            if let url = model.pageURL {
                ReviewWebView(url: url)
            } else {
                VStack(spacing: 14) {
                    Image(systemName: "folder.badge.magnifyingglass")
                        .font(.system(size: 46))
                        .foregroundStyle(.secondary)
                    Text("选择一个项目开始分析")
                        .font(.title2)
                    Text("扫描和图片处理都在这台 Mac 上完成。应用候选前会显示需要修改的文件。")
                        .foregroundStyle(.secondary)
                    Button("选择项目目录") { choosingProject = true }
                        .disabled(model.isStarting)
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity)
            }

            Divider()
            HStack {
                if model.isStarting { ProgressView().controlSize(.small) }
                Text(model.status)
                    .lineLimit(2)
                    .textSelection(.enabled)
                Spacer()
            }
            .font(.caption)
            .foregroundStyle(model.hasError ? .red : .secondary)
            .padding(.horizontal, 16)
            .padding(.vertical, 7)
        }
        .fileImporter(
            isPresented: $choosingProject,
            allowedContentTypes: [.directory]
        ) { result in
            switch result {
            case .success(let url): model.start(project: url, jobs: jobs)
            case .failure(let error): model.fail("无法打开项目目录:\(error.localizedDescription)")
            }
        }
        .fileImporter(
            isPresented: $choosingReport,
            allowedContentTypes: [.directory]
        ) { result in
            switch result {
            case .success(let url): model.open(report: url)
            case .failure(let error): model.fail("无法打开报告目录:\(error.localizedDescription)")
            }
        }
    }
}

private struct ReviewWebView: NSViewRepresentable {
    let url: URL

    func makeCoordinator() -> Coordinator { Coordinator() }

    func makeNSView(context: Context) -> WKWebView {
        let view = WKWebView()
        view.uiDelegate = context.coordinator
        view.load(URLRequest(url: url))
        return view
    }

    func updateNSView(_ view: WKWebView, context: Context) {
        if view.url != url {
            view.load(URLRequest(url: url))
        }
    }

    final class Coordinator: NSObject, WKUIDelegate {
        func webView(
            _ webView: WKWebView,
            createWebViewWith configuration: WKWebViewConfiguration,
            for navigationAction: WKNavigationAction,
            windowFeatures: WKWindowFeatures
        ) -> WKWebView? {
            if navigationAction.targetFrame == nil,
               let url = navigationAction.request.url {
                NSWorkspace.shared.open(url)
            }
            return nil
        }
    }
}

@MainActor
private final class ReviewSession: ObservableObject {
    @Published private(set) var project: URL?
    @Published private(set) var reportDirectory: URL?
    @Published private(set) var pageURL: URL?
    @Published private(set) var status = "就绪。请选择项目目录。"
    @Published private(set) var hasError = false
    @Published private(set) var isStarting = false

    private var process: Process?
    private var stdoutPipe: Pipe?
    private var stderrPipe: Pipe?
    private var stdoutBuffer = Data()
    private var stderrText = ""
    private var scopedProject: URL?
    private var scopedReport: URL?
    private var generation = 0

    func start(project url: URL, jobs: Int) {
        do {
            let reports = try FileManager.default.url(
                for: .applicationSupportDirectory,
                in: .userDomainMask,
                appropriateFor: nil,
                create: true
            ).appendingPathComponent("resopt/Reports", isDirectory: true)
            try FileManager.default.createDirectory(
                at: reports,
                withIntermediateDirectories: true
            )
            let output = reports.appendingPathComponent(UUID().uuidString, isDirectory: true)
            launch(
                arguments: ["web", url.path, "--out", output.path, "--no-open", "--jobs", String(jobs)],
                project: url,
                report: output,
                reportNeedsAccess: false
            )
        } catch {
            fail("启动失败:\(error.localizedDescription)")
        }
    }

    func open(report url: URL) {
        launch(
            arguments: ["serve", url.path],
            project: nil,
            report: url,
            reportNeedsAccess: true
        )
    }

    private func launch(
        arguments: [String],
        project: URL?,
        report: URL,
        reportNeedsAccess: Bool
    ) {
        stop()
        generation += 1
        let currentGeneration = generation
        self.project = project
        pageURL = nil
        reportDirectory = report
        hasError = false
        isStarting = true
        status = project == nil ? "正在打开已有报告…" : "正在启动本地扫描…"
        stdoutBuffer.removeAll()
        stderrText = ""

        if let project, project.startAccessingSecurityScopedResource() {
            scopedProject = project
        }
        if reportNeedsAccess, report.startAccessingSecurityScopedResource() {
            scopedReport = report
        }

        do {
            guard let binary = Bundle.main.url(forResource: "resopt", withExtension: nil),
                  FileManager.default.isExecutableFile(atPath: binary.path) else {
                throw SessionError("App 中缺少 resopt 命令行引擎")
            }
            let child = Process()
            child.executableURL = binary
            child.arguments = arguments
            let stdout = Pipe()
            let stderr = Pipe()
            child.standardOutput = stdout
            child.standardError = stderr
            stdout.fileHandleForReading.readabilityHandler = { [weak self] handle in
                let data = handle.availableData
                if data.isEmpty { handle.readabilityHandler = nil; return }
                Task { @MainActor [weak self] in
                    self?.receiveStdout(data, generation: currentGeneration)
                }
            }
            stderr.fileHandleForReading.readabilityHandler = { [weak self] handle in
                let data = handle.availableData
                if data.isEmpty { handle.readabilityHandler = nil; return }
                Task { @MainActor [weak self] in
                    self?.receiveStderr(data, generation: currentGeneration)
                }
            }
            child.terminationHandler = { [weak self] terminated in
                let code = terminated.terminationStatus
                Task { @MainActor [weak self] in
                    self?.finished(code: code, generation: currentGeneration)
                }
            }
            process = child
            stdoutPipe = stdout
            stderrPipe = stderr
            try child.run()
        } catch {
            stop()
            fail("启动失败:\(error.localizedDescription)")
        }
    }

    func stop() {
        generation += 1
        stdoutPipe?.fileHandleForReading.readabilityHandler = nil
        stderrPipe?.fileHandleForReading.readabilityHandler = nil
        if let process, process.isRunning { process.terminate() }
        process = nil
        stdoutPipe = nil
        stderrPipe = nil
        if let scopedProject {
            scopedProject.stopAccessingSecurityScopedResource()
            self.scopedProject = nil
        }
        if let scopedReport {
            scopedReport.stopAccessingSecurityScopedResource()
            self.scopedReport = nil
        }
        isStarting = false
    }

    func fail(_ message: String) {
        hasError = true
        isStarting = false
        status = message
    }

    private func receiveStdout(_ data: Data, generation: Int) {
        guard generation == self.generation, !data.isEmpty else { return }
        stdoutBuffer.append(data)
        while let newline = stdoutBuffer.firstIndex(of: 10) {
            let line = String(decoding: stdoutBuffer[..<newline], as: UTF8.self)
            stdoutBuffer.removeSubrange(...newline)
            let prefix = line.hasPrefix("Local web: ") ? "Local web: " : "Review server: "
            if line.hasPrefix(prefix),
               let url = URL(string: String(line.dropFirst(prefix.count))),
               url.scheme == "http", url.host == "127.0.0.1" {
                pageURL = url
                isStarting = false
                status = project == nil
                    ? "已打开报告,可以继续审核或恢复之前的操作。"
                    : "正在分析项目;结果会在页面中逐步显示。"
            }
        }
        if stdoutBuffer.count > 16_384 { stdoutBuffer.removeAll() }
    }

    private func receiveStderr(_ data: Data, generation: Int) {
        guard generation == self.generation, !data.isEmpty else { return }
        stderrText += String(decoding: data, as: UTF8.self)
        if stderrText.count > 4_096 { stderrText = String(stderrText.suffix(4_096)) }
    }

    private func finished(code: Int32, generation: Int) {
        guard generation == self.generation else { return }
        process = nil
        isStarting = false
        if code == 0 {
            status = "本地服务已停止。报告保留在 App 的报告目录中。"
        } else {
            fail("本地服务已退出(代码 \(code))。\(stderrText)")
        }
    }
}

private struct SessionError: LocalizedError {
    let message: String
    init(_ message: String) { self.message = message }
    var errorDescription: String? { message }
}