@@ -79,6 +79,50 @@ std::string persistLibraryToDisk(
7979 }
8080}
8181
82+ std::filesystem::path luaEditorRecentPath () {
83+ return PJ::sdk::userDataDir () / " toolbox_reactive_scripts_editor" / " recent.json" ;
84+ }
85+
86+ void persistRecentToDisk (const std::filesystem::path& path, const std::vector<SnippetData>& recent) {
87+ try {
88+ std::filesystem::create_directories (path.parent_path ());
89+ nlohmann::json j = nlohmann::json::array ();
90+ for (const auto & s : recent) {
91+ j.push_back ({{" code" , s.code }, {" global_code" , s.global_code }, {" function_name" , s.function_name }});
92+ }
93+ std::ofstream out (path);
94+ if (out) {
95+ out << j.dump (2 );
96+ }
97+ } catch (...) {}
98+ }
99+
100+ std::vector<SnippetData> loadRecentFromDisk (const std::filesystem::path& path) {
101+ std::vector<SnippetData> result;
102+ std::ifstream in (path);
103+ if (!in) {
104+ return result;
105+ }
106+ std::stringstream buf;
107+ buf << in.rdbuf ();
108+ auto j = nlohmann::json::parse (buf.str (), nullptr , false );
109+ if (!j.is_array ()) {
110+ return result;
111+ }
112+ for (auto & item : j) {
113+ if (!item.is_object ()) {
114+ continue ;
115+ }
116+ result.push_back (
117+ SnippetData{
118+ item.value (" code" , std::string{}),
119+ item.value (" global_code" , std::string{}),
120+ item.value (" function_name" , std::string{}),
121+ });
122+ }
123+ return result;
124+ }
125+
82126std::map<std::string, SnippetData> loadLibraryFromDisk (const std::filesystem::path& path) {
83127 std::map<std::string, SnippetData> result;
84128 std::ifstream in (path);
@@ -245,8 +289,19 @@ class ReactiveScriptEditorDialog : public PJ::DialogPluginTyped {
245289 std::string widget_data () override {
246290 PJ ::WidgetData wd;
247291
248- // -- Timeseries list (left panel) --
249- wd.setListItems (" series_list" , series_names_);
292+ // -- Active Scripts: in-memory only, empty on restart (matches PJ3)
293+ std::vector<std::string> active_names;
294+ for (const auto & s : active_scripts_) {
295+ active_names.push_back (s.function_name );
296+ }
297+ wd.setListItems (" series_list" , active_names);
298+
299+ // -- Recent scripts (independent history with full data copies, matches PJ3 listWidgetRecent)
300+ std::vector<std::string> recent_names;
301+ for (const auto & s : recent_snippets_) {
302+ recent_names.push_back (s.function_name );
303+ }
304+ wd.setListItems (" recent_list" , recent_names);
250305
251306 // -- Language selector --
252307 bool is_lua = (language_ == " lua" );
@@ -265,7 +320,8 @@ class ReactiveScriptEditorDialog : public PJ::DialogPluginTyped {
265320 .setCodeLanguage (" code_editor" , language_)
266321 .setText (" function_name" , function_name_)
267322 .setEnabled (" save_button" , !function_name_.empty () && !code_.empty ())
268- .setEnabled (" run_button" , !function_name_.empty () && !code_.empty () && !has_syntax_error_);
323+ .setEnabled (" run_button" , !function_name_.empty () && !code_.empty () && !has_syntax_error_)
324+ .setEnabled (" deleteButton" , !active_selected_.empty ());
269325
270326 // -- Dynamic labels based on language --
271327 if (is_lua) {
@@ -283,32 +339,15 @@ class ReactiveScriptEditorDialog : public PJ::DialogPluginTyped {
283339 wd.setPlainText (" terminal_output" , terminal_text_);
284340 }
285341
286- // -- Library tab --
287- std::vector<std::string> visible_names;
288- for (const auto & [name, snippet] : saved_snippets_) {
289- if (library_search_.empty () || name.find (library_search_) != std::string::npos) {
290- visible_names.push_back (name);
291- }
292- }
293- wd.setListItems (" library_list" , visible_names);
294- wd.setEnabled (" library_use" , !library_selected_.empty ());
295- wd.setEnabled (" library_delete" , !library_selected_.empty ());
296-
297- // Library preview
298- if (!library_selected_.empty ()) {
299- auto it = saved_snippets_.find (library_selected_);
300- if (it != saved_snippets_.end ()) {
301- std::string preview;
302- if (!it->second .global_code .empty ()) {
303- preview += it->second .global_code + " \n\n " ;
304- }
305- preview += " function " + it->second .function_name + " (tracker_time)\n " ;
306- preview += it->second .code ;
307- preview += " \n end" ;
308- wd.setPlainText (" library_preview" , preview);
309- }
342+ // -- Library tab (matches PJ3 textLibrary) --
343+ wd.setCodeContent (" library_editor" , library_code_).setCodeLanguage (" library_editor" , " lua" );
344+ wd.setEnabled (" library_apply" , !library_valid_);
345+
346+ // Semaphore: green = library OK, red = syntax error
347+ if (library_valid_) {
348+ wd.setText (" labelSemaphore" , " <html><body><span style='color:#00aa00; font-size:22px;'>⬤</span></body></html>" );
310349 } else {
311- wd.setPlainText ( " library_preview " , " " );
350+ wd.setText ( " labelSemaphore " , " <html><body><span style='color:#cc0000; font-size:22px;'>⬤</span></body></html> " );
312351 }
313352
314353 // Tab control (switch to Editor when loading snippet from Library)
@@ -335,6 +374,12 @@ class ReactiveScriptEditorDialog : public PJ::DialogPluginTyped {
335374 terminal_sticky_ = false ;
336375 return true ;
337376 }
377+ if (name == " library_editor" ) {
378+ library_code_ = std::string (code);
379+ // Validate library Lua syntax — pass as global code, empty function body
380+ library_valid_ = validateLuaSyntax (library_code_, " " ).empty ();
381+ return true ;
382+ }
338383 return false ;
339384 }
340385
@@ -343,11 +388,6 @@ class ReactiveScriptEditorDialog : public PJ::DialogPluginTyped {
343388 function_name_ = std::string (text);
344389 return true ;
345390 }
346- if (name == " library_search" ) {
347- library_search_ = std::string (text);
348- library_selected_.clear ();
349- return true ;
350- }
351391 return false ;
352392 }
353393
@@ -375,9 +415,16 @@ class ReactiveScriptEditorDialog : public PJ::DialogPluginTyped {
375415
376416 bool onClicked (std::string_view name) override {
377417 if (name == " save_button" && !function_name_.empty () && !code_.empty ()) {
378- // Save current code to the library (does NOT execute)
379- saved_snippets_[function_name_] = SnippetData{code_, global_code_, function_name_};
380- requestLibraryPersist ();
418+ SnippetData snippet{code_, global_code_, function_name_};
419+ // Add to Active Scripts (in-memory, matches PJ3 listWidgetFunctions)
420+ active_scripts_.erase (
421+ std::remove_if (
422+ active_scripts_.begin (), active_scripts_.end (),
423+ [&](const SnippetData& s) { return s.function_name == function_name_; }),
424+ active_scripts_.end ());
425+ active_scripts_.push_back (snippet);
426+ // Add to Recent scripts (persisted, matches PJ3 listWidgetRecent)
427+ addToRecent (snippet);
381428 return true ;
382429 }
383430 if (name == " run_button" && !function_name_.empty () && !code_.empty ()) {
@@ -395,29 +442,49 @@ class ReactiveScriptEditorDialog : public PJ::DialogPluginTyped {
395442 terminal_sticky_ = true ;
396443 return true ;
397444 }
398- if (name == " library_use" ) {
399- return loadSelectedSnippet ();
445+ // Delete button in Active Scripts (in-memory only, matches PJ3 pushButtonDelete)
446+ if (name == " deleteButton" && !active_selected_.empty ()) {
447+ active_scripts_.erase (
448+ std::remove_if (
449+ active_scripts_.begin (), active_scripts_.end (),
450+ [&](const SnippetData& s) { return s.function_name == active_selected_; }),
451+ active_scripts_.end ());
452+ active_selected_.clear ();
453+ return true ;
400454 }
401- if (name == " library_delete" && !library_selected_.empty ()) {
402- saved_snippets_.erase (library_selected_);
403- library_selected_.clear ();
404- requestLibraryPersist ();
455+ // Restore default library code (matches PJ3 pushButtonDefaultLibrary)
456+ if (name == " library_restore" ) {
457+ library_code_ = kDefaultLibraryCode ;
458+ library_valid_ = true ;
459+ return true ;
460+ }
461+ // Apply changes — mark library as applied (matches PJ3 pushButtonApplyLibrary)
462+ if (name == " library_apply" ) {
463+ library_valid_ = true ;
405464 return true ;
406465 }
407466 return false ;
408467 }
409468
410469 bool onSelectionChanged (std::string_view name, const std::vector<std::string>& items) override {
411- if (name == " library_list" ) {
412- library_selected_ = items.empty () ? " " : items.front ();
470+ // Selecting in either Active Scripts or Recent scripts selects the same snippet
471+ if (name == " series_list" || name == " recent_list" ) {
472+ active_selected_ = items.empty () ? " " : items.front ();
413473 return true ;
414474 }
415475 return false ;
416476 }
417477
418- bool onItemDoubleClicked (std::string_view name, int /* index*/ ) override {
419- if (name == " library_list" ) {
420- return loadSelectedSnippet ();
478+ bool onItemDoubleClicked (std::string_view name, int index) override {
479+ // Double-click on Active Scripts (in-memory) or Recent scripts (PJ3 restoreRecent)
480+ // loads that snippet into the editor.
481+ if (name == " series_list" && index >= 0 && index < static_cast <int >(active_scripts_.size ())) {
482+ applySnippet (active_scripts_[static_cast <size_t >(index)]);
483+ return true ;
484+ }
485+ if (name == " recent_list" && index >= 0 && index < static_cast <int >(recent_snippets_.size ())) {
486+ applySnippet (recent_snippets_[static_cast <size_t >(index)]);
487+ return true ;
421488 }
422489 return false ;
423490 }
@@ -509,8 +576,6 @@ class ReactiveScriptEditorDialog : public PJ::DialogPluginTyped {
509576 terminal_visible_ = false ;
510577 terminal_text_.clear ();
511578 validation_pending_ = false ;
512- library_selected_.clear ();
513- library_search_.clear ();
514579 switch_to_tab_ = -1 ;
515580 return true ;
516581 }
@@ -549,6 +614,23 @@ class ReactiveScriptEditorDialog : public PJ::DialogPluginTyped {
549614 saved_snippets_ = std::move (snippets);
550615 }
551616
617+ void setRecentSnippets (std::vector<SnippetData> recent) {
618+ recent_snippets_ = std::move (recent);
619+ }
620+
621+ void addToRecent (const SnippetData& snippet) {
622+ recent_snippets_.erase (
623+ std::remove_if (
624+ recent_snippets_.begin (), recent_snippets_.end (),
625+ [&](const SnippetData& s) { return s.function_name == snippet.function_name ; }),
626+ recent_snippets_.end ());
627+ recent_snippets_.insert (recent_snippets_.begin (), snippet);
628+ if (recent_snippets_.size () > 10 ) {
629+ recent_snippets_.resize (10 );
630+ }
631+ persistRecentToDisk (luaEditorRecentPath (), recent_snippets_);
632+ }
633+
552634 // Persistence callback: called by the dialog after a save/delete in the library.
553635 // The toolbox provides this to flush the library to disk and surface errors via the runtime host.
554636 using LibrarySaveCallback = std::function<void (const std::map<std::string, SnippetData>&)>;
@@ -567,34 +649,64 @@ class ReactiveScriptEditorDialog : public PJ::DialogPluginTyped {
567649 }
568650 }
569651
570- bool loadSelectedSnippet () {
571- if (library_selected_.empty ()) {
572- return false ;
573- }
574- auto it = saved_snippets_.find (library_selected_);
575- if (it == saved_snippets_.end ()) {
576- return false ;
577- }
578-
579- code_ = it->second .code ;
580- global_code_ = it->second .global_code ;
581- function_name_ = it->second .function_name ;
582- switch_to_tab_ = 0 ; // Switch back to Editor tab
652+ // Load a snippet's code/global/name into the editor and switch to the Editor tab.
653+ void applySnippet (const SnippetData& s) {
654+ code_ = s.code ;
655+ global_code_ = s.global_code ;
656+ function_name_ = s.function_name ;
657+ switch_to_tab_ = 0 ;
583658 validation_pending_ = true ;
584659 validation_tick_counter_ = 0 ;
585- return true ;
586660 }
587661
588- std::string code_ =
589- " -- Write your Lua function body here.\n "
590- " -- It receives tracker_time as parameter.\n "
591- " -- Example:\n "
592- " -- local series = TimeseriesView(\" my_field\" )\n "
593- " -- local val = series:atTime(tracker_time)\n " ;
662+ std::string code_;
594663 std::string global_code_;
595664 std::string function_name_;
596665 std::string language_ = " lua" ;
597666
667+ // Library code (matches PJ3 textLibrary) — editable block of Lua helper functions
668+ static constexpr const char * kDefaultLibraryCode =
669+ " -- Helper functions usable in your reactive scripts\n "
670+ " \n "
671+ " function CreateSeriesFromArray(new_series, prefix, suffix_X, suffix_Y, timestamp)\n "
672+ " new_series:clear()\n "
673+ " local index = 0\n "
674+ " while true do\n "
675+ " local x = index\n "
676+ " if suffix_X ~= nil then\n "
677+ " local series_x = TimeseriesView.find(string.format(\" %s.%d/%s\" , prefix, index, suffix_X))\n "
678+ " if series_x == nil then break end\n "
679+ " x = series_x:atTime(timestamp)\n "
680+ " end\n "
681+ " local series_y = TimeseriesView.find(string.format(\" %s.%d/%s\" , prefix, index, suffix_Y))\n "
682+ " if series_y == nil then break end\n "
683+ " local y = series_y:atTime(timestamp)\n "
684+ " new_series:push_back(x, y)\n "
685+ " index = index + 1\n "
686+ " end\n "
687+ " end\n "
688+ " \n "
689+ " function GetSeriesNamesByPrefix(prefix)\n "
690+ " local all_names = GetSeriesNames()\n "
691+ " local filtered = {}\n "
692+ " for i, name in ipairs(all_names) do\n "
693+ " if name:find(prefix, 1, #prefix) then\n "
694+ " table.insert(filtered, name)\n "
695+ " end\n "
696+ " end\n "
697+ " return filtered\n "
698+ " end\n "
699+ " \n "
700+ " function ApplyOffsetInPlace(series, delta_x, delta_y)\n "
701+ " for index=0, series:size()-1 do\n "
702+ " local x, y = series:at(index)\n "
703+ " series:set(index, x + delta_x, y + delta_y)\n "
704+ " end\n "
705+ " end\n " ;
706+
707+ std::string library_code_ = kDefaultLibraryCode ;
708+ bool library_valid_ = true ; // true when library_code_ compiles without errors
709+
598710 // Terminal / validation
599711 std::string terminal_text_;
600712 bool terminal_visible_ = false ;
@@ -613,8 +725,11 @@ class ReactiveScriptEditorDialog : public PJ::DialogPluginTyped {
613725 // Library
614726 std::map<std::string, SnippetData> saved_snippets_;
615727 std::map<std::string, SnippetData> legacy_library_snippets_; // Set by loadConfig() for one-shot migration.
616- std::string library_search_;
617- std::string library_selected_;
728+ std::string active_selected_; // selected in series_list or recent_list
729+ // Active Scripts: in-memory only, empty on restart (matches PJ3 listWidgetFunctions)
730+ std::vector<SnippetData> active_scripts_;
731+ // Recent history: persisted, survives restart (matches PJ3 listWidgetRecent)
732+ std::vector<SnippetData> recent_snippets_;
618733 LibrarySaveCallback library_save_callback_;
619734 int switch_to_tab_ = -1 ; // -1 = no programmatic switch
620735};
@@ -1058,7 +1173,11 @@ class ReactiveScriptEditorToolbox : public PJ::ToolboxPluginBase {
10581173 if (library_loaded_) {
10591174 return ;
10601175 }
1176+ // Active Scripts: loaded from library.json (persists saves/deletes)
10611177 dialog_.setLibrary (loadLibraryFromDisk (luaEditorLibraryPath ()));
1178+ // Recent scripts: independent history from recent.json — survives deletion from Active Scripts
1179+ // On startup Active Scripts may be empty (like PJ3) while Recent scripts keeps the history
1180+ dialog_.setRecentSnippets (loadRecentFromDisk (luaEditorRecentPath ()));
10621181 library_loaded_ = true ;
10631182 }
10641183
0 commit comments