Skip to main content

maolan_plugin_host/
scan.rs

1use crate::clap::{
2    CLAP_EXT_AUDIO_PORTS, CLAP_EXT_PARAMS, CLAP_VERSION, ClapAudioPortInfo, ClapHost,
3    ClapParamInfo, ClapPluginAudioPorts, ClapPluginEntry, ClapPluginFactory, ClapPluginParams,
4};
5use serde::{Deserialize, Serialize};
6use std::ffi::{CStr, CString, c_char, c_void};
7use std::path::{Path, PathBuf};
8use std::ptr;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ParamMetadata {
12    pub id: u32,
13    pub name: String,
14    pub module: String,
15    pub min_value: f64,
16    pub max_value: f64,
17    pub default_value: f64,
18    pub flags: u32,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct AudioPortMetadata {
23    pub id: u32,
24    pub name: String,
25    pub channel_count: u32,
26    pub flags: u32,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct PluginMetadata {
31    pub id: String,
32    pub name: String,
33    pub vendor: String,
34    pub version: String,
35    pub description: String,
36    pub params: Vec<ParamMetadata>,
37    pub audio_inputs: Vec<AudioPortMetadata>,
38    pub audio_outputs: Vec<AudioPortMetadata>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct ScanResult {
43    pub format: String,
44    pub path: String,
45    pub plugins: Vec<PluginMetadata>,
46    pub error: Option<String>,
47}
48
49#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
50pub struct ClapPluginInfo {
51    pub name: String,
52    pub path: String,
53    pub capabilities: Option<ClapPluginCapabilities>,
54}
55
56#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
57pub struct ClapPluginCapabilities {
58    pub has_gui: bool,
59    pub gui_apis: Vec<String>,
60    pub supports_embedded: bool,
61    pub supports_floating: bool,
62    pub has_params: bool,
63    pub has_state: bool,
64    pub audio_inputs: usize,
65    pub audio_outputs: usize,
66    pub midi_inputs: usize,
67    pub midi_outputs: usize,
68}
69
70unsafe extern "C" fn dummy_get_extension(_: *const ClapHost, _: *const c_char) -> *const c_void {
71    ptr::null()
72}
73unsafe extern "C" fn dummy_request_restart(_: *const ClapHost) {}
74unsafe extern "C" fn dummy_request_process(_: *const ClapHost) {}
75unsafe extern "C" fn dummy_request_callback(_: *const ClapHost) {}
76
77fn cstr_to_string(ptr: *const c_char) -> String {
78    if ptr.is_null() {
79        return String::new();
80    }
81    unsafe { CStr::from_ptr(ptr) }
82        .to_string_lossy()
83        .into_owned()
84}
85
86fn clap_version_is_compatible(plugin_version: &crate::clap::ClapVersion) -> bool {
87    plugin_version.major == crate::clap::CLAP_VERSION.major
88        && plugin_version.minor <= crate::clap::CLAP_VERSION.minor
89}
90
91pub fn scan_clap_plugin(plugin_path: &str) -> ScanResult {
92    let path = Path::new(plugin_path);
93    if !path.exists() {
94        return ScanResult {
95            format: "clap".to_string(),
96            path: plugin_path.to_string(),
97            plugins: Vec::new(),
98            error: Some(format!("path does not exist: {plugin_path}")),
99        };
100    }
101
102    let library = match unsafe { libloading::Library::new(path) } {
103        Ok(lib) => lib,
104        Err(e) => {
105            return ScanResult {
106                format: "clap".to_string(),
107                path: plugin_path.to_string(),
108                plugins: Vec::new(),
109                error: Some(format!("failed to load library: {e}")),
110            };
111        }
112    };
113
114    let entry: libloading::Symbol<*const ClapPluginEntry> =
115        match unsafe { library.get(b"clap_entry\0") } {
116            Ok(sym) => sym,
117            Err(e) => {
118                return ScanResult {
119                    format: "clap".to_string(),
120                    path: plugin_path.to_string(),
121                    plugins: Vec::new(),
122                    error: Some(format!("clap_entry not found: {e}")),
123                };
124            }
125        };
126
127    let entry = unsafe { &**entry };
128
129    if let Some(init) = entry.init {
130        let path_c = match CString::new(plugin_path) {
131            Ok(s) => s,
132            Err(_) => {
133                return ScanResult {
134                    format: "clap".to_string(),
135                    path: plugin_path.to_string(),
136                    plugins: Vec::new(),
137                    error: Some("plugin path contains null bytes".to_string()),
138                };
139            }
140        };
141        if !unsafe { init(path_c.as_ptr()) } {
142            return ScanResult {
143                format: "clap".to_string(),
144                path: plugin_path.to_string(),
145                plugins: Vec::new(),
146                error: Some("clap_entry.init() failed".to_string()),
147            };
148        }
149    }
150
151    let factory = if let Some(get_factory) = entry.get_factory {
152        let factory_id = CString::new("clap.plugin-factory").unwrap();
153        let factory_ptr = unsafe { get_factory(factory_id.as_ptr()) };
154        if factory_ptr.is_null() {
155            return ScanResult {
156                format: "clap".to_string(),
157                path: plugin_path.to_string(),
158                plugins: Vec::new(),
159                error: Some("clap.plugin-factory not found".to_string()),
160            };
161        }
162        unsafe { &*(factory_ptr as *const ClapPluginFactory) }
163    } else {
164        return ScanResult {
165            format: "clap".to_string(),
166            path: plugin_path.to_string(),
167            plugins: Vec::new(),
168            error: Some("clap_entry.get_factory is null".to_string()),
169        };
170    };
171
172    let count = factory
173        .get_plugin_count
174        .map(|f| unsafe { f(factory) })
175        .unwrap_or(0);
176
177    let mut host = ClapHost {
178        clap_version: CLAP_VERSION,
179        host_data: ptr::null_mut(),
180        name: c"maolan-plugin-host".as_ptr(),
181        vendor: c"Maolan".as_ptr(),
182        url: c"https://maolan.github.io".as_ptr(),
183        version: c"0.1.0".as_ptr(),
184        get_extension: Some(dummy_get_extension),
185        request_restart: Some(dummy_request_restart),
186        request_process: Some(dummy_request_process),
187        request_callback: Some(dummy_request_callback),
188    };
189    host.host_data = (&mut host as *mut ClapHost).cast::<c_void>();
190
191    let mut plugins = Vec::with_capacity(count as usize);
192
193    for i in 0..count {
194        let desc = factory
195            .get_plugin_descriptor
196            .map(|f| unsafe { f(factory, i) })
197            .unwrap_or(ptr::null());
198        if desc.is_null() {
199            continue;
200        }
201        let desc = unsafe { &*desc };
202
203        if !clap_version_is_compatible(&desc.clap_version) {
204            continue;
205        }
206
207        let plugin_id = cstr_to_string(desc.id);
208        let plugin_id_c = match CString::new(&*plugin_id) {
209            Ok(s) => s,
210            Err(_) => continue,
211        };
212
213        let plugin = factory
214            .create_plugin
215            .map(|f| unsafe { f(factory, &host, plugin_id_c.as_ptr()) })
216            .unwrap_or(ptr::null());
217        if plugin.is_null() {
218            continue;
219        }
220
221        let init_ok = unsafe { (*plugin).init }
222            .map(|f| unsafe { f(plugin) })
223            .unwrap_or(false);
224        if !init_ok {
225            unsafe {
226                if let Some(destroy) = (*plugin).destroy {
227                    destroy(plugin);
228                }
229            }
230            continue;
231        }
232
233        let mut params = Vec::new();
234        let mut audio_inputs = Vec::new();
235        let mut audio_outputs = Vec::new();
236
237        unsafe {
238            let ext = (*plugin)
239                .get_extension
240                .map(|f| f(plugin, CLAP_EXT_PARAMS.as_ptr()));
241            if let Some(ptr) = ext
242                && !ptr.is_null()
243            {
244                let p = &*(ptr as *const ClapPluginParams);
245                let count = p.count.map(|f| f(plugin)).unwrap_or(0);
246                for pi in 0..count {
247                    let mut info = ClapParamInfo {
248                        id: 0,
249                        flags: 0,
250                        cookie: ptr::null_mut(),
251                        name: [0; 256],
252                        module: [0; 1024],
253                        min_value: 0.0,
254                        max_value: 0.0,
255                        default_value: 0.0,
256                    };
257                    if p.get_info
258                        .map(|f| f(plugin, pi, &mut info))
259                        .unwrap_or(false)
260                    {
261                        let name = CStr::from_ptr(info.name.as_ptr())
262                            .to_string_lossy()
263                            .into_owned();
264                        let module = CStr::from_ptr(info.module.as_ptr())
265                            .to_string_lossy()
266                            .into_owned();
267                        params.push(ParamMetadata {
268                            id: info.id,
269                            name,
270                            module,
271                            min_value: info.min_value,
272                            max_value: info.max_value,
273                            default_value: info.default_value,
274                            flags: info.flags,
275                        });
276                    }
277                }
278            }
279        }
280
281        unsafe {
282            let ext = (*plugin)
283                .get_extension
284                .map(|f| f(plugin, CLAP_EXT_AUDIO_PORTS.as_ptr()));
285            if let Some(ptr) = ext
286                && !ptr.is_null()
287            {
288                let ap = &*(ptr as *const ClapPluginAudioPorts);
289                let in_count = ap.count.map(|f| f(plugin, true)).unwrap_or(0);
290                let out_count = ap.count.map(|f| f(plugin, false)).unwrap_or(0);
291                for pi in 0..in_count {
292                    let mut info = ClapAudioPortInfo {
293                        id: 0,
294                        name: [0; 256],
295                        flags: 0,
296                        channel_count: 0,
297                        port_type: ptr::null(),
298                        in_place_pair: 0,
299                    };
300                    if ap
301                        .get
302                        .map(|f| f(plugin, pi, true, &mut info))
303                        .unwrap_or(false)
304                    {
305                        let name = CStr::from_ptr(info.name.as_ptr())
306                            .to_string_lossy()
307                            .into_owned();
308                        audio_inputs.push(AudioPortMetadata {
309                            id: info.id,
310                            name,
311                            channel_count: info.channel_count,
312                            flags: info.flags,
313                        });
314                    }
315                }
316                for pi in 0..out_count {
317                    let mut info = ClapAudioPortInfo {
318                        id: 0,
319                        name: [0; 256],
320                        flags: 0,
321                        channel_count: 0,
322                        port_type: ptr::null(),
323                        in_place_pair: 0,
324                    };
325                    if ap
326                        .get
327                        .map(|f| f(plugin, pi, false, &mut info))
328                        .unwrap_or(false)
329                    {
330                        let name = CStr::from_ptr(info.name.as_ptr())
331                            .to_string_lossy()
332                            .into_owned();
333                        audio_outputs.push(AudioPortMetadata {
334                            id: info.id,
335                            name,
336                            channel_count: info.channel_count,
337                            flags: info.flags,
338                        });
339                    }
340                }
341            }
342        }
343
344        plugins.push(PluginMetadata {
345            id: plugin_id,
346            name: cstr_to_string(desc.name),
347            vendor: cstr_to_string(desc.vendor),
348            version: cstr_to_string(desc.version),
349            description: cstr_to_string(desc.description),
350            params,
351            audio_inputs,
352            audio_outputs,
353        });
354
355        unsafe {
356            if let Some(destroy) = (*plugin).destroy {
357                destroy(plugin);
358            }
359        }
360    }
361
362    if let Some(deinit) = entry.deinit {
363        unsafe { deinit() };
364    }
365
366    ScanResult {
367        format: "clap".to_string(),
368        path: plugin_path.to_string(),
369        plugins,
370        error: None,
371    }
372}
373
374#[cfg(any(
375    target_os = "macos",
376    target_os = "linux",
377    target_os = "freebsd",
378    target_os = "openbsd"
379))]
380fn default_clap_search_roots() -> Vec<PathBuf> {
381    let mut roots = Vec::new();
382    #[cfg(target_os = "macos")]
383    {
384        crate::paths::push_macos_audio_plugin_roots(&mut roots, "CLAP");
385    }
386    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))]
387    {
388        crate::paths::push_unix_plugin_roots(&mut roots, "clap");
389    }
390    roots
391}
392
393#[cfg(windows)]
394fn default_clap_search_roots() -> Vec<PathBuf> {
395    let mut roots = Vec::new();
396    crate::paths::push_windows_clap_roots(&mut roots);
397    roots
398}
399
400#[cfg(not(any(
401    target_os = "macos",
402    target_os = "linux",
403    target_os = "freebsd",
404    target_os = "openbsd",
405    target_os = "windows"
406)))]
407fn default_clap_search_roots() -> Vec<PathBuf> {
408    Vec::new()
409}
410
411fn is_supported_clap_binary(path: &Path) -> bool {
412    path.extension()
413        .is_some_and(|ext| ext.eq_ignore_ascii_case("clap"))
414}
415
416fn scan_clap_bundle(path: &Path, scan_capabilities: bool) -> Vec<ClapPluginInfo> {
417    use crate::clap::{
418        ClapPluginAudioPorts, ClapPluginEntry, ClapPluginFactory, ClapPluginGui,
419        ClapPluginNotePorts, ClapPluginParams,
420    };
421
422    let path_str = path.to_string_lossy().to_string();
423    let factory_id = c"clap.plugin-factory";
424    let mut host = ClapHost {
425        clap_version: CLAP_VERSION,
426        host_data: ptr::null_mut(),
427        name: c"Maolan".as_ptr(),
428        vendor: c"Maolan".as_ptr(),
429        url: c"https://example.invalid".as_ptr(),
430        version: c"0.0.1".as_ptr(),
431        get_extension: Some(dummy_get_extension),
432        request_restart: Some(dummy_request_restart),
433        request_process: Some(dummy_request_process),
434        request_callback: Some(dummy_request_callback),
435    };
436    host.host_data = (&mut host as *mut ClapHost).cast::<c_void>();
437
438    let lib = match unsafe { libloading::Library::new(path) } {
439        Ok(l) => l,
440        Err(_) => {
441            return vec![ClapPluginInfo {
442                name: path
443                    .file_stem()
444                    .map(|s| s.to_string_lossy().to_string())
445                    .unwrap_or_else(|| path_str.clone()),
446                path: path_str,
447                capabilities: None,
448            }];
449        }
450    };
451
452    let entry: libloading::Symbol<*const ClapPluginEntry> =
453        match unsafe { lib.get(b"clap_entry\0") } {
454            Ok(sym) => sym,
455            Err(_) => {
456                return vec![ClapPluginInfo {
457                    name: path
458                        .file_stem()
459                        .map(|s| s.to_string_lossy().to_string())
460                        .unwrap_or_else(|| path_str.clone()),
461                    path: path_str,
462                    capabilities: None,
463                }];
464            }
465        };
466    let entry = unsafe { &**entry };
467
468    if let Some(init) = entry.init {
469        let path_c = match CString::new(&*path_str) {
470            Ok(s) => s,
471            Err(_) => {
472                return vec![ClapPluginInfo {
473                    name: path_str.clone(),
474                    path: path_str,
475                    capabilities: None,
476                }];
477            }
478        };
479        if !unsafe { init(path_c.as_ptr()) } {
480            return vec![ClapPluginInfo {
481                name: path_str.clone(),
482                path: path_str,
483                capabilities: None,
484            }];
485        }
486    }
487
488    let factory = if let Some(get_factory) = entry.get_factory {
489        let factory_ptr = unsafe { get_factory(factory_id.as_ptr()) };
490        if factory_ptr.is_null() {
491            return vec![ClapPluginInfo {
492                name: path_str.clone(),
493                path: path_str,
494                capabilities: None,
495            }];
496        }
497        unsafe { &*(factory_ptr as *const ClapPluginFactory) }
498    } else {
499        return vec![ClapPluginInfo {
500            name: path_str.clone(),
501            path: path_str,
502            capabilities: None,
503        }];
504    };
505
506    let count = factory
507        .get_plugin_count
508        .map(|f| unsafe { f(factory) })
509        .unwrap_or(0);
510
511    let mut out = Vec::with_capacity(count as usize);
512
513    for i in 0..count {
514        let desc = factory
515            .get_plugin_descriptor
516            .map(|f| unsafe { f(factory, i) })
517            .unwrap_or(ptr::null());
518        if desc.is_null() {
519            continue;
520        }
521        let desc = unsafe { &*desc };
522        if !clap_version_is_compatible(&desc.clap_version) {
523            continue;
524        }
525        let name = cstr_to_string(desc.name);
526        let plugin_id = cstr_to_string(desc.id);
527        let plugin_id_c = match CString::new(&*plugin_id) {
528            Ok(s) => s,
529            Err(_) => continue,
530        };
531
532        let plugin = factory
533            .create_plugin
534            .map(|f| unsafe { f(factory, &host, plugin_id_c.as_ptr()) })
535            .unwrap_or(ptr::null());
536        if plugin.is_null() {
537            continue;
538        }
539        let init_ok = unsafe { (*plugin).init }
540            .map(|f| unsafe { f(plugin) })
541            .unwrap_or(false);
542        if !init_ok {
543            unsafe {
544                if let Some(destroy) = (*plugin).destroy {
545                    destroy(plugin);
546                }
547            }
548            continue;
549        }
550
551        let mut capabilities = None;
552        if scan_capabilities {
553            let mut caps = ClapPluginCapabilities {
554                has_gui: false,
555                gui_apis: Vec::new(),
556                supports_embedded: false,
557                supports_floating: false,
558                has_params: false,
559                has_state: false,
560                audio_inputs: 0,
561                audio_outputs: 0,
562                midi_inputs: 0,
563                midi_outputs: 0,
564            };
565
566            unsafe {
567                let ext = (*plugin)
568                    .get_extension
569                    .map(|f| f(plugin, c"clap.gui".as_ptr()));
570                if let Some(ptr) = ext
571                    && !ptr.is_null()
572                {
573                    let gui = &*(ptr as *const ClapPluginGui);
574                    caps.has_gui = gui
575                        .is_api_supported
576                        .map(|f| f(plugin, c"x11".as_ptr(), true))
577                        .unwrap_or(false)
578                        || gui
579                            .is_api_supported
580                            .map(|f| f(plugin, c"win32".as_ptr(), true))
581                            .unwrap_or(false)
582                        || gui
583                            .is_api_supported
584                            .map(|f| f(plugin, c"cocoa".as_ptr(), true))
585                            .unwrap_or(false);
586                    if caps.has_gui {
587                        caps.gui_apis = vec!["x11".to_string()];
588                        caps.supports_embedded = true;
589                        caps.supports_floating = gui
590                            .is_api_supported
591                            .map(|f| f(plugin, ptr::null(), false))
592                            .unwrap_or(false);
593                    }
594                }
595            }
596
597            unsafe {
598                let ext = (*plugin)
599                    .get_extension
600                    .map(|f| f(plugin, CLAP_EXT_PARAMS.as_ptr()));
601                if let Some(ptr) = ext
602                    && !ptr.is_null()
603                {
604                    let p = &*(ptr as *const ClapPluginParams);
605                    caps.has_params = p.count.map(|f| f(plugin)).unwrap_or(0) > 0;
606                }
607            }
608
609            unsafe {
610                let ext = (*plugin)
611                    .get_extension
612                    .map(|f| f(plugin, c"clap.state".as_ptr()));
613                if let Some(ptr) = ext
614                    && !ptr.is_null()
615                {
616                    caps.has_state = true;
617                }
618            }
619
620            unsafe {
621                let ext = (*plugin)
622                    .get_extension
623                    .map(|f| f(plugin, CLAP_EXT_AUDIO_PORTS.as_ptr()));
624                if let Some(ptr) = ext
625                    && !ptr.is_null()
626                {
627                    let ap = &*(ptr as *const ClapPluginAudioPorts);
628                    let in_count = ap.count.map(|f| f(plugin, true)).unwrap_or(0);
629                    let out_count = ap.count.map(|f| f(plugin, false)).unwrap_or(0);
630                    for pi in 0..in_count {
631                        let mut info = ClapAudioPortInfo {
632                            id: 0,
633                            name: [0; 256],
634                            flags: 0,
635                            channel_count: 0,
636                            port_type: ptr::null(),
637                            in_place_pair: 0,
638                        };
639                        if ap
640                            .get
641                            .map(|f| f(plugin, pi, true, &mut info))
642                            .unwrap_or(false)
643                        {
644                            caps.audio_inputs += info.channel_count as usize;
645                        }
646                    }
647                    for pi in 0..out_count {
648                        let mut info = ClapAudioPortInfo {
649                            id: 0,
650                            name: [0; 256],
651                            flags: 0,
652                            channel_count: 0,
653                            port_type: ptr::null(),
654                            in_place_pair: 0,
655                        };
656                        if ap
657                            .get
658                            .map(|f| f(plugin, pi, false, &mut info))
659                            .unwrap_or(false)
660                        {
661                            caps.audio_outputs += info.channel_count as usize;
662                        }
663                    }
664                }
665            }
666
667            unsafe {
668                let ext = (*plugin)
669                    .get_extension
670                    .map(|f| f(plugin, c"clap.note-ports".as_ptr()));
671                if let Some(ptr) = ext
672                    && !ptr.is_null()
673                {
674                    let np = &*(ptr as *const ClapPluginNotePorts);
675                    caps.midi_inputs = np.count.map(|f| f(plugin, true)).unwrap_or(0) as usize;
676                    caps.midi_outputs = np.count.map(|f| f(plugin, false)).unwrap_or(0) as usize;
677                }
678            }
679
680            capabilities = Some(caps);
681        }
682
683        unsafe {
684            if let Some(destroy) = (*plugin).destroy {
685                destroy(plugin);
686            }
687        }
688
689        out.push(ClapPluginInfo {
690            name,
691            path: format!("{}::{}", path_str, plugin_id),
692            capabilities,
693        });
694    }
695
696    if let Some(deinit) = entry.deinit {
697        unsafe { deinit() };
698    }
699
700    if out.is_empty() {
701        out.push(ClapPluginInfo {
702            name: path
703                .file_stem()
704                .map(|s| s.to_string_lossy().to_string())
705                .unwrap_or_else(|| path_str.clone()),
706            path: path_str,
707            capabilities: None,
708        });
709    }
710
711    out
712}
713
714fn collect_clap_plugins(root: &Path, out: &mut Vec<ClapPluginInfo>, scan_capabilities: bool) {
715    let Ok(entries) = std::fs::read_dir(root) else {
716        return;
717    };
718    for entry in entries.flatten() {
719        let path = entry.path();
720        let Ok(ft) = entry.file_type() else {
721            continue;
722        };
723        if ft.is_dir() {
724            if path
725                .file_name()
726                .and_then(|name| name.to_str())
727                .is_some_and(|name| {
728                    matches!(
729                        name,
730                        "deps" | "build" | "incremental" | ".fingerprint" | "examples"
731                    )
732                })
733            {
734                continue;
735            }
736            collect_clap_plugins(&path, out, scan_capabilities);
737            continue;
738        }
739
740        if is_supported_clap_binary(&path) {
741            let infos = scan_clap_bundle(&path, scan_capabilities);
742            if infos.is_empty() {
743                let name = path
744                    .file_stem()
745                    .map(|s| s.to_string_lossy().to_string())
746                    .unwrap_or_else(|| path.to_string_lossy().to_string());
747                out.push(ClapPluginInfo {
748                    name,
749                    path: path.to_string_lossy().to_string(),
750                    capabilities: None,
751                });
752            } else {
753                out.extend(infos);
754            }
755        }
756    }
757}
758
759pub fn scan_clap_plugins(scan_capabilities: bool) -> Vec<ClapPluginInfo> {
760    let mut roots = default_clap_search_roots();
761
762    if let Ok(extra) = std::env::var("CLAP_PATH") {
763        for p in std::env::split_paths(&extra) {
764            if !p.as_os_str().is_empty() {
765                roots.push(p);
766            }
767        }
768    }
769
770    let mut out = Vec::new();
771    for root in roots {
772        collect_clap_plugins(&root, &mut out, scan_capabilities);
773    }
774
775    out.sort_by_key(|a| a.name.to_lowercase());
776    out.dedup_by(|a, b| {
777        a.name.eq_ignore_ascii_case(&b.name) && a.path.eq_ignore_ascii_case(&b.path)
778    });
779    out
780}
781
782pub fn scan_vst3_plugins() -> Vec<crate::vst3::Vst3PluginInfo> {
783    crate::vst3::host::Vst3Host::new().list_plugins()
784}
785
786#[cfg(unix)]
787pub fn scan_lv2_plugins() -> Vec<crate::lv2::Lv2PluginInfo> {
788    crate::lv2::Lv2Host::new(48_000.0).list_plugins()
789}
790
791pub fn run_scan(format: &str, plugin_path: &str, output_path: Option<&str>) -> i32 {
792    let json = match format {
793        "clap" => {
794            if plugin_path == "--system" {
795                match serde_json::to_string_pretty(&scan_clap_plugins(false)) {
796                    Ok(j) => j,
797                    Err(_e) => {
798                        return 1;
799                    }
800                }
801            } else {
802                match serde_json::to_string_pretty(&scan_clap_plugin(plugin_path)) {
803                    Ok(j) => j,
804                    Err(_e) => {
805                        return 1;
806                    }
807                }
808            }
809        }
810        "vst3" => {
811            if plugin_path != "--system" {
812                return 1;
813            }
814            match serde_json::to_string_pretty(&scan_vst3_plugins()) {
815                Ok(j) => j,
816                Err(_e) => {
817                    return 1;
818                }
819            }
820        }
821        #[cfg(unix)]
822        "lv2" => {
823            if plugin_path != "--system" {
824                return 1;
825            }
826            match serde_json::to_string_pretty(&scan_lv2_plugins()) {
827                Ok(j) => j,
828                Err(_e) => {
829                    return 1;
830                }
831            }
832        }
833        _ => {
834            return 1;
835        }
836    };
837
838    if let Some(path) = output_path {
839        match std::fs::write(path, &json) {
840            Ok(()) => 0,
841            Err(_e) => 1,
842        }
843    } else {
844        0
845    }
846}