feat(plugins): search and filter the installed plugin library (phase 84)

This commit is contained in:
Johannes Findeisen 2026-11-06 01:38:07 +01:00
commit 2ecc9df343
4 changed files with 57 additions and 26 deletions

View file

@ -1940,12 +1940,12 @@ Implemented: Ctrl+4 opens the track insert rack and actual generated modular gra
Make LADSPA, VST2, VST3 and CLAP effects easy to find and add, including Linux VST3 bundles, format filters and clear compatibility errors.
### Tasks and acceptance criteria
- [ ] Implement the described workflow with validated errors and safe ownership.
- [ ] Pass relevant core and/or GUI tests.
- [ ] Document behavior and limitations; commit this phase separately.
- [x] Implement the described workflow with validated errors and safe ownership.
- [x] Pass relevant core and/or GUI tests.
- [x] Document behavior and limitations; commit this phase separately.
### Implementation status
Planned.
Implemented: case-insensitive path/name search and All/LADSPA/VST2/VST3/CLAP filters in the shared plugin library, with recursive Linux VST3 bundle discovery, deduplication and bounded traversal. Listing executes no module code; inspection checks ABI/layout. Ambiguous .so candidates appear in both LADSPA and VST2 filters. Warning-enabled GUI build and focused discovery-filter/adoption tests passed.
---

View file

@ -34,3 +34,8 @@ the graph displays the actual generated graph compiled by playback. Wiring in
this view is read-only: edit the rack to preserve the serial chain. The global
Modular Graph remains independent. A failed plugin probe or playback preparation
preserves the current project; successful additions are undoable.
The library searches configured Preferences paths recursively (including VST3
bundle contents), with case-insensitive search and format filters. LADSPA and
VST2 both use `.so`, so those filters share candidates until inspection identifies
the ABI. Listing files executes no plugin code. Only inspect trusted binaries.

View file

@ -5,6 +5,10 @@
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Hold_Browser.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Input.H>
#include <FL/Fl_Choice.H>
#include <algorithm>
#include <cctype>
#include <FL/Fl_Native_File_Chooser.H>
#include <filesystem>
#include <set>
@ -42,33 +46,50 @@ inline std::vector<std::string> xtc_plugin_files(const xtc_settings &settings)
}
return {files.begin(),files.end()};
}
/* Paths are a discovery hint, never a compatibility claim. Ambiguous .so files
* remain visible under both LADSPA and VST2; probing identifies the real ABI. */
inline bool xtc_plugin_file_matches(const std::string &file,const std::string &query,int format)
{
auto lower=[](std::string value){for(char &c:value)c=static_cast<char>(std::tolower(static_cast<unsigned char>(c)));return value;};
const auto name=lower(file);bool vst3=name.find(".vst3/")!=std::string::npos;
bool clap=std::filesystem::path(name).extension()==".clap";
if(format==3 && !vst3)return false;
if(format==4 && !clap)return false;
if((format==1 || format==2) && (vst3 || clap))return false;
return name.find(lower(query))!=std::string::npos;
}
inline bool xtc_choose_plugin_file(const xtc_settings &settings,std::string &path)
{
struct Library : Fl_Double_Window {
bool accepted=false,browse=false;
Library():Fl_Double_Window(900,520,"Plugin library — trusted binaries") {set_modal();}
} dialog;
auto files=xtc_plugin_files(settings);
auto *hint=new Fl_Box(16,10,868,44,"Files from Preferences search paths. Select a trusted binary to inspect its plugins.\nNo module code is executed while listing. Browse can select another binary.");
hint->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE);
auto *list=new Fl_Hold_Browser(16,62,868,370); list->format_char(0);
for(const auto &file:files) list->add(file.c_str());
if(!files.empty()) list->select(1);
auto *browse=new Fl_Button(16,462,130,32,"Browse files...");
browse->callback([](Fl_Widget *,void *c){auto *d=static_cast<Library *>(c);d->browse=true;d->hide();},&dialog);
auto *cancel=new Fl_Button(650,462,100,32,"Cancel");
cancel->callback([](Fl_Widget *,void *c){static_cast<Library *>(c)->hide();},&dialog);
auto *choose=new Fl_Button(764,462,120,32,"Inspect plugin");
choose->callback([](Fl_Widget *,void *c){auto *d=static_cast<Library *>(c);d->accepted=true;d->hide();},&dialog);
if(files.empty()) choose->deactivate();
dialog.callback([](Fl_Widget *,void *c){static_cast<Library *>(c)->hide();},&dialog);
dialog.end(); xtc_ui::style(&dialog); dialog.show(); while(dialog.shown()) Fl::wait();
Fl_Input *search;Fl_Choice *format;Fl_Hold_Browser *list;
Fl_Button *choose;std::vector<std::string> files,visible;
explicit Library(const xtc_settings &settings):Fl_Double_Window(900,560,"Plugin library — effects and instruments"),files(xtc_plugin_files(settings)) {
set_modal();
auto *hint=new Fl_Box(16,8,868,42,"Search installed binaries; VST3 bundles are discovered recursively. Modules load only when inspected.\nLADSPA/VST2 share .so candidates; inspection verifies the format and compatible mono layout.");hint->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE);
search=new Fl_Input(78,60,510,28,"Search");search->when(FL_WHEN_CHANGED);
format=new Fl_Choice(666,60,218,28,"Format");format->add("All formats|LADSPA|VST2|VST3|CLAP");format->value(0);
for(auto *w:{static_cast<Fl_Widget *>(search),static_cast<Fl_Widget *>(format)})w->callback([](Fl_Widget *,void *c){static_cast<Library *>(c)->filter();},this);
list=new Fl_Hold_Browser(16,102,868,370);list->format_char(0);
auto *browse_button=new Fl_Button(16,506,130,32,"Browse files...");browse_button->callback([](Fl_Widget *,void *c){auto *d=static_cast<Library *>(c);d->browse=true;d->hide();},this);
auto *cancel=new Fl_Button(650,506,100,32,"Cancel");cancel->callback([](Fl_Widget *,void *c){static_cast<Library *>(c)->hide();},this);
choose=new Fl_Button(764,506,120,32,"Inspect plugin");choose->callback([](Fl_Widget *,void *c){auto *d=static_cast<Library *>(c);d->accepted=true;d->hide();},this);
callback([](Fl_Widget *,void *c){static_cast<Library *>(c)->hide();},this);
end();filter();xtc_ui::style(this);
}
void filter() {
list->clear();visible.clear();
for(const auto &file:files)if(xtc_plugin_file_matches(file,search->value(),format->value())){visible.push_back(file);list->add(file.c_str());}
if(visible.empty())choose->deactivate();else{choose->activate();list->select(1);}
}
} dialog(settings);
dialog.show();while(dialog.shown())Fl::wait();
if(dialog.browse) {
Fl_Native_File_Chooser chooser; chooser.title("Choose trusted plugin binary"); chooser.filter("Plugin binaries\t*.{so,clap}\nAll files\t*");
if(chooser.show()!=0 || !chooser.filename()) return false;
path=chooser.filename(); return true;
Fl_Native_File_Chooser chooser;chooser.title("Choose trusted plugin binary");chooser.filter("Plugin binaries\t*.{so,clap}\nAll files\t*");
if(chooser.show()!=0 || !chooser.filename())return false;
path=chooser.filename();return true;
}
if(!dialog.accepted || list->value()<1) return false;
path=files[static_cast<size_t>(list->value()-1)]; return true;
if(!dialog.accepted || dialog.list->value()<1)return false;
path=dialog.visible[static_cast<size_t>(dialog.list->value()-1)];return true;
}
#endif

View file

@ -218,6 +218,11 @@ static int sample_editor_tests()
}
int track_insert_tests()
{
CHECK(xtc_plugin_file_matches("/usr/lib/vst3/ZamEQ2.vst3/Contents/x86_64-linux/ZamEQ2.so","zameq",3));
CHECK(!xtc_plugin_file_matches("/usr/lib/vst3/ZamEQ2.vst3/Contents/x86_64-linux/ZamEQ2.so","zameq",4));
CHECK(xtc_plugin_file_matches("/usr/lib/clap/room.clap","ROOM",4));
CHECK(xtc_plugin_file_matches("/usr/lib/ladspa/gain.so","gain",1) && xtc_plugin_file_matches("/usr/lib/vst/gain.so","gain",2));
CHECK(!xtc_plugin_file_matches("/usr/lib/vst/gain.so","absent",0));
reset();MainWindow window(false,"");window.show();pump();auto *s=timeline(&window);
CHECK(window.create_track(false)==XTC_OK);auto track=s->selected_track();auto *panel=window.track_inserts();CHECK(panel);
panel->select_track(track);CHECK(panel->selected_track()==track && panel->graph());