diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 058fe16..ebb8e1c 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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. --- diff --git a/docs/TRACK_INSERTS.md b/docs/TRACK_INSERTS.md index fa9124c..3b3df4b 100644 --- a/docs/TRACK_INSERTS.md +++ b/docs/TRACK_INSERTS.md @@ -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. diff --git a/src/gui/plugin_library.hpp b/src/gui/plugin_library.hpp index 9a15286..20d28f8 100644 --- a/src/gui/plugin_library.hpp +++ b/src/gui/plugin_library.hpp @@ -5,6 +5,10 @@ #include #include #include +#include +#include +#include +#include #include #include #include @@ -42,33 +46,50 @@ inline std::vector 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(std::tolower(static_cast(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(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(c)->hide();},&dialog); - auto *choose=new Fl_Button(764,462,120,32,"Inspect plugin"); - choose->callback([](Fl_Widget *,void *c){auto *d=static_cast(c);d->accepted=true;d->hide();},&dialog); - if(files.empty()) choose->deactivate(); - dialog.callback([](Fl_Widget *,void *c){static_cast(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 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(search),static_cast(format)})w->callback([](Fl_Widget *,void *c){static_cast(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(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(c)->hide();},this); + choose=new Fl_Button(764,506,120,32,"Inspect plugin");choose->callback([](Fl_Widget *,void *c){auto *d=static_cast(c);d->accepted=true;d->hide();},this); + callback([](Fl_Widget *,void *c){static_cast(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(list->value()-1)]; return true; + if(!dialog.accepted || dialog.list->value()<1)return false; + path=dialog.visible[static_cast(dialog.list->value()-1)];return true; } #endif diff --git a/tests/integration/test_gui.cpp b/tests/integration/test_gui.cpp index 221c802..1f12388 100644 --- a/tests/integration/test_gui.cpp +++ b/tests/integration/test_gui.cpp @@ -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());