88import SwiftUI
99import JavaScriptCore
1010
11+ typealias RemoteServerHandle = OpaquePointer
12+ typealias ScreenshotClientHandle = OpaquePointer
13+
1114class RunJSViewModel : ObservableObject {
1215 var context : JSContext ?
1316 @Published var logs : [ String ] = [ ]
1417 @Published var scriptName : String = " Script "
1518 @Published var executionInterrupted = false
1619 var pid : Int
1720 var debugProxy : OpaquePointer ?
21+ var remoteServer : OpaquePointer ?
1822 var semaphore : dispatch_semaphore_t ?
19- private var progressTimer : DispatchSourceTimer ?
20- private var reportedProgress : Double = 0
2123
22- init ( pid: Int , debugProxy: OpaquePointer ? , semaphore: dispatch_semaphore_t ? ) {
24+ init ( pid: Int , debugProxy: OpaquePointer ? , remoteServer : OpaquePointer ? , semaphore: dispatch_semaphore_t ? ) {
2325 self . pid = pid
2426 self . debugProxy = debugProxy
27+ self . remoteServer = remoteServer
2528 self . semaphore = semaphore
2629 }
2730
@@ -32,7 +35,6 @@ class RunJSViewModel: ObservableObject {
3235 func runScript( data: Data , name: String ? = nil ) throws {
3336 let scriptContent = String ( data: data, encoding: . utf8)
3437 scriptName = name ?? " Script "
35- startContinuedProcessing ( withTitle: scriptName)
3638
3739 let getPidFunction : @convention ( block) ( ) -> Int = {
3840 return self . pid
@@ -61,6 +63,10 @@ class RunJSViewModel: ObservableObject {
6163 return handleJITPageWrite ( self . context, startAddr, regionSize, self . debugProxy) ?? " "
6264 }
6365
66+ let takeScreenshotFunction : @convention ( block) ( String ? ) -> String ? = { fileName in
67+ return self . captureScreenshot ( named: fileName)
68+ }
69+
6470 let hasTXMFunction : @convention ( block) ( ) -> Bool = {
6571 return ProcessInfo . processInfo. hasTXM
6672 }
@@ -70,6 +76,7 @@ class RunJSViewModel: ObservableObject {
7076 context? . setObject ( getPidFunction, forKeyedSubscript: " get_pid " as NSString )
7177 context? . setObject ( sendCommandFunction, forKeyedSubscript: " send_command " as NSString )
7278 context? . setObject ( prepareMemoryRegionFunction, forKeyedSubscript: " prepare_memory_region " as NSString )
79+ context? . setObject ( takeScreenshotFunction, forKeyedSubscript: " take_screenshot " as NSString )
7380 context? . setObject ( logFunction, forKeyedSubscript: " log " as NSString )
7481
7582 context? . evaluateScript ( scriptContent)
@@ -81,43 +88,139 @@ class RunJSViewModel: ObservableObject {
8188 if let exception = self . context? . exception {
8289 self . logs. append ( exception. debugDescription)
8390 }
84- let success = self . context? . exception == nil && !self . executionInterrupted
85- self . stopContinuedProcessing ( success: success)
8691 self . logs. append ( " Script Execution Completed " )
87- self . logs. append ( " Background processing finished. You can dismiss this view. " )
92+ self . logs. append ( " You are safe to close the PIP Window. " )
93+ }
94+ }
95+
96+ private func captureScreenshot( named preferredName: String ? ) -> String {
97+ if executionInterrupted {
98+ raiseException ( " Script execution is interrupted by StikDebug. " )
99+ return " "
100+ }
101+ guard let remoteServer else {
102+ raiseException ( " Screenshot capture is unavailable in the current session. " )
103+ return " "
104+ }
105+
106+ var screenshotClient : ScreenshotClientHandle ?
107+ let creationError = screenshot_client_new ( remoteServer, & screenshotClient)
108+ if let creationError {
109+ let message = describeIdeviceError ( creationError)
110+ idevice_error_free ( creationError)
111+ raiseException ( " Failed to create screenshot client: \( message) " )
112+ return " "
113+ }
114+ guard let screenshotClient else {
115+ raiseException ( " Failed to allocate screenshot client. " )
116+ return " "
117+ }
118+ defer { screenshot_client_free ( screenshotClient) }
119+
120+ var buffer : UnsafeMutablePointer < UInt8 > ?
121+ var length : UInt = 0
122+ let captureError = screenshot_client_take_screenshot ( screenshotClient, & buffer, & length)
123+ if let captureError {
124+ let message = describeIdeviceError ( captureError)
125+ idevice_error_free ( captureError)
126+ raiseException ( " Failed to take screenshot: \( message) " )
127+ return " "
128+ }
129+ guard let buffer else {
130+ raiseException ( " Device returned empty screenshot data. " )
131+ return " "
132+ }
133+ defer { idevice_data_free ( buffer, length) }
134+
135+ let data = Data ( bytes: buffer, count: Int ( length) )
136+ do {
137+ let fileURL = try screenshotFileURL ( preferredName: preferredName)
138+ try data. write ( to: fileURL, options: . atomic)
139+ return fileURL. path
140+ } catch {
141+ raiseException ( " Failed to save screenshot: \( error. localizedDescription) " )
142+ return " "
143+ }
144+ }
145+
146+ private func screenshotFileURL( preferredName: String ? ) throws -> URL {
147+ let directory = URL . documentsDirectory. appendingPathComponent ( " screenshots " , isDirectory: true )
148+ try FileManager . default. createDirectory ( at: directory, withIntermediateDirectories: true )
149+ let fileManager = FileManager . default
150+ let initialName = sanitizedScreenshotName ( from: preferredName)
151+ var targetURL = directory. appendingPathComponent ( initialName)
152+ guard fileManager. fileExists ( atPath: targetURL. path) else {
153+ return targetURL
88154 }
155+
156+ let baseName = targetURL. deletingPathExtension ( ) . lastPathComponent
157+ let ext = targetURL. pathExtension. isEmpty ? " png " : targetURL. pathExtension
158+ var counter = 1
159+ repeat {
160+ let candidate = " \( baseName) - \( counter) . \( ext) "
161+ targetURL = directory. appendingPathComponent ( candidate)
162+ counter += 1
163+ } while fileManager. fileExists ( atPath: targetURL. path)
164+ return targetURL
89165 }
90166
91- private func startContinuedProcessing ( withTitle title : String ) {
92- guard ContinuedProcessingManager . shared . isSupported ,
93- UserDefaults . standard . bool ( forKey : UserDefaults . Keys . enableContinuedProcessing ) else { return }
94- stopProgressTimer ( )
95- reportedProgress = 0.05
96- ContinuedProcessingManager . shared . begin ( title : title , subtitle : " Script execution in progress " )
97- ContinuedProcessingManager . shared . updateProgress ( reportedProgress )
98- let timer = DispatchSource . makeTimerSource ( queue : DispatchQueue . global ( qos : . background ) )
99- timer . schedule ( deadline : . now ( ) + 5 , repeating : 5 )
100- timer . setEventHandler { [ weak self ] in
101- guard let self else { return }
102- self . reportedProgress = min ( 0.9 , self . reportedProgress + 0.1 )
103- ContinuedProcessingManager . shared . updateProgress ( self . reportedProgress )
104- if self . reportedProgress >= 0.9 {
105- self . stopProgressTimer ( )
167+ private func sanitizedScreenshotName ( from preferredName : String ? ) -> String {
168+ let defaultName = " screenshot- \( Int ( Date ( ) . timeIntervalSince1970 ) ) "
169+ guard var candidate = preferredName ? . trimmingCharacters ( in : . whitespacesAndNewlines ) ,
170+ !candidate . isEmpty else {
171+ return " \( defaultName ) .png "
172+ }
173+
174+ let allowed = CharacterSet . alphanumerics . union ( CharacterSet ( charactersIn : " -_. " ) )
175+ var sanitized = " "
176+ sanitized . reserveCapacity ( candidate . count )
177+ for scalar in candidate . unicodeScalars {
178+ if allowed . contains ( scalar ) {
179+ sanitized . append ( Character ( scalar ) )
180+ } else {
181+ sanitized . append ( " _ " )
106182 }
107183 }
108- timer. resume ( )
109- progressTimer = timer
184+ if sanitized. isEmpty {
185+ sanitized = defaultName
186+ }
187+ if !sanitized. lowercased ( ) . hasSuffix ( " .png " ) {
188+ sanitized += " .png "
189+ }
190+ return sanitized
191+ }
192+
193+ private func describeIdeviceError( _ error: UnsafeMutablePointer < IdeviceFfiError > ) -> String {
194+ if let messagePointer = error. pointee. message {
195+ return " [ \( error. pointee. code) ] \( String ( cString: messagePointer) ) "
196+ }
197+ return " [ \( error. pointee. code) ] Unknown error "
110198 }
111199
112- private func stopContinuedProcessing( success: Bool ) {
113- stopProgressTimer ( )
114- ContinuedProcessingManager . shared. updateProgress ( 1.0 )
115- ContinuedProcessingManager . shared. finish ( success: success)
200+ private func raiseException( _ message: String ) {
201+ guard let context else { return }
202+ context. exception = JSValue ( object: message, in: context)
116203 }
204+ }
117205
118- private func stopProgressTimer( ) {
119- progressTimer? . cancel ( )
120- progressTimer = nil
206+ struct RunJSViewPiP : View {
207+ @Binding var model : RunJSViewModel ?
208+ @State private var logs : [ String ] = [ ]
209+ private let timer = Timer . publish ( every: 0.034 , on: . main, in: . common) . autoconnect ( )
210+
211+ var body : some View {
212+ VStack ( alignment: . leading, spacing: 4 ) {
213+ ForEach ( logs. suffix ( 6 ) . indices, id: \. self) { index in
214+ Text ( logs. suffix ( 6 ) [ index] )
215+ . font ( . system( size: 12 ) )
216+ . foregroundStyle ( . white)
217+ }
218+ }
219+ . padding ( )
220+ . onReceive ( timer) { _ in
221+ logs = model? . logs ?? [ ]
222+ }
223+ . frame ( width: 300 , height: 150 )
121224 }
122225}
123226
0 commit comments