Skip to main content

Mountain/IPC/WindServiceHandlers/NativeDialog/
ShowOpenDialog.rs

1//! `nativeHost:showOpenDialog` handler. Wires VS Code's
2//! `nativeHostService.showOpenDialog(options)` contract to Tauri's
3//! dialog plugin.
4//!
5//! Contract:
6//!   - `properties: ["openDirectory" | "openFile" | "multiSelections" |
7//!     "createDirectory" | "showHiddenFiles"]`
8//!   - `filters: [{ name, extensions: ["vsix", …] }, …]`
9//!   - `title`, `buttonLabel`, `defaultPath`
10//!   - returns `{ canceled: bool, filePaths: string[] }`.
11//!
12//! The "Install from VSIX…" flow relies on `filters` to narrow the picker
13//! to `.vsix` and on `openFile + multiSelections` so the user can pick
14//! multiple archives at once.
15
16use serde_json::{Value, json};
17use tauri::AppHandle;
18
19use crate::{IPC::WindServiceHandlers::NativeDialog::ParseDialogFilters::Fn as ParseDialogFilters, dev_log};
20
21pub async fn Fn(ApplicationHandle:AppHandle, Args:Vec<Value>) -> Result<Value, String> {
22	use tauri_plugin_dialog::DialogExt;
23
24	dev_log!("folder", "showOpenDialog: {:?}", Args);
25
26	// Electron passes `(windowId, options)`; `options` is always the last
27	// element regardless of how the renderer was invoked. Searching by
28	// shape (`first object with a 'properties' or 'filters' field`) keeps
29	// us robust against VS Code versions that pass an extra prefix arg.
30	let Options = Args.iter().rev().find(|V| V.is_object()).cloned().unwrap_or(Value::Null);
31
32	let Properties:Vec<String> = Options
33		.get("properties")
34		.and_then(Value::as_array)
35		.map(|Array| Array.iter().filter_map(|V| V.as_str().map(str::to_string)).collect())
36		.unwrap_or_default();
37
38	let IsFolder = Properties.iter().any(|P| P == "openDirectory");
39
40	let IsMultiple = Properties.iter().any(|P| P == "multiSelections");
41
42	let Title = Options
43		.get("title")
44		.and_then(Value::as_str)
45		.unwrap_or(if IsFolder { "Open Folder" } else { "Open File" })
46		.to_string();
47
48	let DefaultPath = Options.get("defaultPath").and_then(Value::as_str).map(str::to_string);
49
50	let Filters = ParseDialogFilters(&Options);
51
52	let Handle = ApplicationHandle.clone();
53
54	let FiltersForThread = Filters.clone();
55
56	let Selected = tokio::task::spawn_blocking(move || -> Vec<String> {
57		let mut Builder = Handle.dialog().file().set_title(&Title);
58
59		if let Some(Path) = DefaultPath.as_deref() {
60			Builder = Builder.set_directory(Path);
61		}
62
63		// Apply filters only for file pickers - Tauri returns an error on
64		// folder pickers if filters are set on some platforms.
65		if !IsFolder {
66			for Filter in &FiltersForThread {
67				let ExtRefs:Vec<&str> = Filter.Extensions.iter().map(String::as_str).collect();
68
69				Builder = Builder.add_filter(&Filter.Name, &ExtRefs);
70			}
71		}
72
73		if IsFolder {
74			if IsMultiple {
75				Builder
76					.blocking_pick_folders()
77					.unwrap_or_default()
78					.into_iter()
79					.map(|P| P.to_string())
80					.collect()
81			} else {
82				Builder.blocking_pick_folder().map(|P| vec![P.to_string()]).unwrap_or_default()
83			}
84		} else if IsMultiple {
85			Builder
86				.blocking_pick_files()
87				.unwrap_or_default()
88				.into_iter()
89				.map(|P| P.to_string())
90				.collect()
91		} else {
92			Builder.blocking_pick_file().map(|P| vec![P.to_string()]).unwrap_or_default()
93		}
94	})
95	.await
96	.map_err(|Error| format!("showOpenDialog join error: {}", Error))?;
97
98	if Selected.is_empty() {
99		dev_log!("folder", "showOpenDialog cancelled by user");
100
101		Ok(json!({ "canceled": true, "filePaths": [] }))
102	} else {
103		dev_log!(
104			"folder",
105			"showOpenDialog selected {} path(s) (folder={}, multi={}, filters={})",
106			Selected.len(),
107			IsFolder,
108			IsMultiple,
109			Filters.len()
110		);
111
112		Ok(json!({ "canceled": false, "filePaths": Selected }))
113	}
114}