@@ -110,4 +110,111 @@ public static class OpenFileDialog
110110 return Task . FromResult < string ? > ( null ) ;
111111 }
112112 }
113+
114+ public static Task < string [ ] ? > OpenFilesAsync ( )
115+ {
116+ if ( OperatingSystem . IsWindows ( ) )
117+ {
118+ return OpenFilesAsyncWindows ( ) ;
119+ }
120+ else if ( OperatingSystem . IsMacOS ( ) )
121+ {
122+ return OpenFilesAsyncMacOS ( ) ;
123+ }
124+ else if ( OperatingSystem . IsLinux ( ) )
125+ {
126+ return OpenFilesAsyncLinux ( ) ;
127+ }
128+ else
129+ {
130+ return Task . FromResult < string [ ] ? > ( null ) ;
131+ }
132+ }
133+
134+ [ SupportedOSPlatform ( "windows" ) ]
135+ private unsafe static Task < string [ ] ? > OpenFilesAsyncWindows ( )
136+ {
137+ // https://learn.microsoft.com/en-us/windows/win32/api/commdlg/ns-commdlg-openfilenamew
138+
139+ char [ ] buffer = ArrayPool < char > . Shared . Rent ( ushort . MaxValue + 1 ) ; // Should be enough for the overwhelming majority of cases.
140+ new Span < char > ( buffer ) . Clear ( ) ;
141+
142+ string filter = "All Files\0 *.*\0 \0 " ;
143+
144+ fixed ( char * bufferPtr = buffer )
145+ fixed ( char * filterPtr = filter )
146+ {
147+ OPENFILENAMEW ofn = default ;
148+ ofn . lStructSize = ( uint ) Unsafe . SizeOf < OPENFILENAMEW > ( ) ;
149+ ofn . hwndOwner = default ; // No owner window.
150+ ofn . lpstrFile = bufferPtr ;
151+ ofn . nMaxFile = ( uint ) buffer . Length ;
152+ ofn . lpstrFilter = filterPtr ;
153+ ofn . nFilterIndex = 1 ; // The first pair of strings has an index value of 1.
154+ ofn . Flags = OFN . OFN_PATHMUSTEXIST | OFN . OFN_FILEMUSTEXIST | OFN . OFN_ALLOWMULTISELECT | OFN . OFN_EXPLORER ;
155+ if ( Windows . GetOpenFileNameW ( & ofn ) && buffer [ ^ 1 ] == 0 )
156+ {
157+ List < string > files = [ ] ;
158+
159+ int directoryLength = Array . IndexOf ( buffer , '\0 ' ) ;
160+ string directory = new ( buffer , 0 , directoryLength ) ;
161+
162+ int startIndex = directoryLength + 1 ;
163+ while ( startIndex < buffer . Length && buffer [ startIndex ] != '\0 ' )
164+ {
165+ int endIndex = Array . IndexOf ( buffer , '\0 ' , startIndex ) ;
166+ string fileName = new ( buffer , startIndex , endIndex - startIndex ) ;
167+ files . Add ( Path . Combine ( directory , fileName ) ) ;
168+ startIndex = endIndex + 1 ; // Move to the next file name
169+ }
170+
171+ ArrayPool < char > . Shared . Return ( buffer ) ;
172+ if ( files . Count > 0 )
173+ {
174+ return Task . FromResult < string [ ] ? > ( files . ToArray ( ) ) ;
175+ }
176+ else
177+ {
178+ // If a single file was selected, the system appends it to the directory path.
179+ return Task . FromResult < string [ ] ? > ( [ directory ] ) ;
180+ }
181+ }
182+ }
183+
184+ ArrayPool < char > . Shared . Return ( buffer ) ;
185+ return Task . FromResult < string [ ] ? > ( null ) ;
186+ }
187+
188+ [ SupportedOSPlatform ( "macos" ) ]
189+ private static async Task < string [ ] ? > OpenFilesAsyncMacOS ( )
190+ {
191+ ReadOnlySpan < string > arguments =
192+ [
193+ "-e" , "set theFiles to choose file with multiple selections allowed" ,
194+ "-e" , "set filePaths to {}" ,
195+ "-e" , "repeat with aFile in theFiles" ,
196+ "-e" , "set end of filePaths to POSIX path of aFile" ,
197+ "-e" , "end repeat" ,
198+ "-e" , "set text item delimiters to \" :\" " ,
199+ "-e" , "return filePaths as string" ,
200+ ] ;
201+ string ? output = await ProcessExecutor . TryRun ( "osascript" , arguments ) ;
202+ if ( string . IsNullOrEmpty ( output ) )
203+ {
204+ return null ; // User canceled the dialog
205+ }
206+ return output . Split ( ':' ) ;
207+ }
208+
209+ [ SupportedOSPlatform ( "linux" ) ]
210+ private static async Task < string [ ] ? > OpenFilesAsyncLinux ( )
211+ {
212+ // Todo: proper Linux implementation
213+ string ? path = await OpenFileDialog . OpenFileAsync ( ) ;
214+ if ( string . IsNullOrEmpty ( path ) )
215+ {
216+ return null ; // User canceled the dialog
217+ }
218+ return [ path ] ;
219+ }
113220}
0 commit comments