From c3db83cdc93509fb242c9f5d62f2a2f3e21d376c Mon Sep 17 00:00:00 2001 From: yum Date: Sat, 4 Feb 2023 14:48:44 -0800 Subject: GUI: Add debug panel Add debug panel with options to show installed packages, clear the pip cache, reset venv, and clear OSC configs. * Refactor synchronous command execution + logging pattern inside PythonWrapper --- GUI/GUI/GUI/Frame.cpp | 164 +++++++++++++++++++++++++++++++++ GUI/GUI/GUI/Frame.h | 7 ++ GUI/GUI/GUI/PythonWrapper.cpp | 210 +++++++++--------------------------------- GUI/GUI/GUI/PythonWrapper.h | 3 + 4 files changed, 220 insertions(+), 164 deletions(-) (limited to 'GUI') diff --git a/GUI/GUI/GUI/Frame.cpp b/GUI/GUI/GUI/Frame.cpp index e0663c5..75d9e82 100644 --- a/GUI/GUI/GUI/Frame.cpp +++ b/GUI/GUI/GUI/Frame.cpp @@ -16,6 +16,7 @@ namespace { ID_NAVBAR, ID_NAVBAR_BUTTON_TRANSCRIBE, ID_NAVBAR_BUTTON_UNITY, + ID_NAVBAR_BUTTON_DEBUG, ID_PY_PANEL, ID_PY_CONFIG_PANEL, ID_PY_APP_CONFIG_PANEL_PAIRS, @@ -57,6 +58,13 @@ namespace { ID_UNITY_BYTES_PER_CHAR, ID_UNITY_ROWS, ID_UNITY_COLS, + ID_DEBUG_PANEL, + ID_DEBUG_OUT, + ID_DEBUG_CONFIG_PANEL, + ID_DEBUG_BUTTON_CLEAR_PIP, + ID_DEBUG_BUTTON_LIST_PIP, + ID_DEBUG_BUTTON_RESET_VENV, + ID_DEBUG_BUTTON_CLEAR_OSC, }; const wxString kMicChoices[] = { @@ -278,12 +286,14 @@ Frame::Frame() { auto* navbar_button_transcribe = new wxButton(navbar, ID_NAVBAR_BUTTON_TRANSCRIBE, "Transcription"); auto* navbar_button_unity = new wxButton(navbar, ID_NAVBAR_BUTTON_UNITY, "Unity"); + auto* navbar_button_debug = new wxButton(navbar, ID_NAVBAR_BUTTON_DEBUG, "Debug"); auto* sizer = new wxBoxSizer(wxVERTICAL); navbar->SetSizer(sizer); sizer->Add(navbar_button_transcribe, /*proportion=*/0, /*flags=*/wxEXPAND); sizer->Add(navbar_button_unity, /*proportion=*/0, /*flags=*/wxEXPAND); + sizer->Add(navbar_button_debug, /*proportion=*/0, /*flags=*/wxEXPAND); } auto* transcribe_panel = new wxPanel(main_panel, ID_PY_PANEL); @@ -706,22 +716,93 @@ Frame::Frame() } unity_panel_->Hide(); + auto* debug_panel = new wxPanel(main_panel, ID_DEBUG_PANEL); + debug_panel_ = debug_panel; + { + const auto debug_out_sz = wxSize(/*x_px=*/480, /*y_px=*/160); + auto* debug_out = new wxTextCtrl(debug_panel, ID_DEBUG_OUT, + wxEmptyString, + wxDefaultPosition, + debug_out_sz, wxTE_MULTILINE | wxTE_READONLY); + debug_out->SetMinSize(debug_out_sz); + debug_out_ = debug_out; + + auto* debug_config_panel = new wxPanel(debug_panel, ID_DEBUG_CONFIG_PANEL); + { + auto* debug_button_list_pip = new wxButton(debug_config_panel, ID_DEBUG_BUTTON_LIST_PIP, "List pip packages"); + debug_button_list_pip->SetToolTip( + "List the packages (and versions) installed in the " + "virtual environment by pip. Also list the contents " + "of the pip cache."); + debug_button_list_pip->SetWindowStyleFlag(wxBU_EXACTFIT); + + auto* debug_button_clear_pip = new wxButton(debug_config_panel, ID_DEBUG_BUTTON_CLEAR_PIP, "Clear pip cache"); + // The real explanation: we install a special version of torch + // using --extra-index-url, and I'm like 99% sure that pip + // doesn't correctly detect that we want this version instead + // of the normal version. + debug_button_clear_pip->SetToolTip( + "TaSTT uses a piece of software called pip to install " + "Python dependencies. To enable reusing packages across " + "different Python projects, pip installs packages in a " + "system-wide cache. Sometimes the contents of this cache " + "can get stale (it's complicated) and clearing the cache " + "can fix issues."); + debug_button_clear_pip->SetWindowStyleFlag(wxBU_EXACTFIT); + + auto* debug_button_reset_venv = new wxButton(debug_config_panel, ID_DEBUG_BUTTON_RESET_VENV, "Reset python virtual environment"); + debug_button_reset_venv->SetToolTip( + "Uninstall all Python packages installed into the virtual " + "environment. Do this after clearing pip!"); + debug_button_reset_venv->SetWindowStyleFlag(wxBU_EXACTFIT); + + auto* debug_button_clear_osc = new wxButton(debug_config_panel, ID_DEBUG_BUTTON_CLEAR_OSC, "Clear OSC configs"); + debug_button_clear_osc->SetToolTip( + "No idea if this actually does anything valuable yet. I " + "think making certain animator changes (s.a. turning on " + "multi-byte character encoding) require you to reset " + "(i.e. delete) your OSC config. This button deletes all " + "your OSC configs."); + debug_button_clear_osc->SetWindowStyleFlag(wxBU_EXACTFIT); + + auto* sizer = new wxBoxSizer(wxVERTICAL); + debug_config_panel->SetSizer(sizer); + sizer->Add(debug_button_list_pip, /*proportion=*/0, /*flags=*/wxEXPAND); + sizer->Add(debug_button_clear_pip, /*proportion=*/0, /*flags=*/wxEXPAND); + sizer->Add(debug_button_reset_venv, /*proportion=*/0, /*flags=*/wxEXPAND); + sizer->Add(debug_button_clear_osc, /*proportion=*/0, /*flags=*/wxEXPAND); + } + + auto* sizer = new wxBoxSizer(wxHORIZONTAL); + debug_panel->SetSizer(sizer); + sizer->Add(debug_config_panel, /*proportion=*/0, /*flags=*/wxEXPAND); + sizer->Add(debug_out, /*proportion=*/1, /*flags=*/wxEXPAND); + } + debug_panel_->Hide(); + auto* sizer = new wxBoxSizer(wxHORIZONTAL); main_panel->SetSizer(sizer); sizer->Add(navbar, /*proportion=*/0, /*flags=*/wxEXPAND); sizer->Add(transcribe_panel, /*proportion=*/1, /*flags=*/wxEXPAND); sizer->Add(unity_panel, /*proportion=*/1, /*flags=*/wxEXPAND); + sizer->Add(debug_panel, /*proportion=*/1, /*flags=*/wxEXPAND); } Bind(wxEVT_MENU, &Frame::OnExit, this, wxID_EXIT); Bind(wxEVT_BUTTON, &Frame::OnNavbarTranscribe, this, ID_NAVBAR_BUTTON_TRANSCRIBE); Bind(wxEVT_BUTTON, &Frame::OnNavbarUnity, this, ID_NAVBAR_BUTTON_UNITY); + Bind(wxEVT_BUTTON, &Frame::OnNavbarDebug, this, ID_NAVBAR_BUTTON_DEBUG); Bind(wxEVT_BUTTON, &Frame::OnAppStart, this, ID_PY_APP_START_BUTTON); Bind(wxEVT_BUTTON, &Frame::OnAppStop, this, ID_PY_APP_STOP_BUTTON); Bind(wxEVT_TIMER, &Frame::OnAppDrain, this, ID_PY_APP_DRAIN); Bind(wxEVT_BUTTON, &Frame::OnSetupPython, this, ID_PY_SETUP_BUTTON); Bind(wxEVT_BUTTON, &Frame::OnDumpMics, this, ID_PY_DUMP_MICS_BUTTON); Bind(wxEVT_BUTTON, &Frame::OnGenerateFX, this, ID_UNITY_BUTTON_GEN_ANIMATOR); + Bind(wxEVT_BUTTON, &Frame::OnListPip, this, ID_DEBUG_BUTTON_LIST_PIP); + Bind(wxEVT_BUTTON, &Frame::OnClearPip, this, ID_DEBUG_BUTTON_CLEAR_PIP); + Bind(wxEVT_BUTTON, &Frame::OnListPip, this, ID_DEBUG_BUTTON_LIST_PIP); + Bind(wxEVT_BUTTON, &Frame::OnResetVenv, this, ID_DEBUG_BUTTON_RESET_VENV); + Bind(wxEVT_BUTTON, &Frame::OnClearOSC, this, ID_DEBUG_BUTTON_CLEAR_OSC); Bind(wxEVT_CHOICE, &Frame::OnUnityParamChange, this, ID_UNITY_CHARS_PER_SYNC); Bind(wxEVT_CHOICE, &Frame::OnUnityParamChange, this, ID_UNITY_BYTES_PER_CHAR); @@ -746,6 +827,7 @@ void Frame::OnNavbarTranscribe(wxCommandEvent& event) { transcribe_panel_->Show(); unity_panel_->Hide(); + debug_panel_->Hide(); Resize(); } @@ -753,6 +835,15 @@ void Frame::OnNavbarUnity(wxCommandEvent& event) { transcribe_panel_->Hide(); unity_panel_->Show(); + debug_panel_->Hide(); + Resize(); +} + +void Frame::OnNavbarDebug(wxCommandEvent& event) +{ + transcribe_panel_->Hide(); + unity_panel_->Hide(); + debug_panel_->Show(); Resize(); } @@ -899,6 +990,79 @@ void Frame::OnGenerateFX(wxCommandEvent& event) } } +void Frame::OnListPip(wxCommandEvent& event) +{ + Log(debug_out_, "Listing pip packages... "); + PythonWrapper::InvokeWithArgs({ + "-m pip", + "list", + }, "Failed to list pip packages", debug_out_); + + Log(debug_out_, "Listing pip cache... "); + PythonWrapper::InvokeWithArgs({ + "-m pip", + "cache", + "list", + }, "Failed to list pip cache", debug_out_); +} + +void Frame::OnClearPip(wxCommandEvent& event) +{ + Log(debug_out_, "Clearing pip cache... "); + PythonWrapper::InvokeWithArgs({ + "-m pip", + "cache", + "purge", + }, "Failed to clear pip cache", debug_out_); +} + +void Frame::OnResetVenv(wxCommandEvent& event) +{ + Log(debug_out_, "Resetting virtual environment... "); + + const std::string py_dir = "Resources/Python/Lib/site-packages"; + + if (!std::filesystem::is_directory(py_dir)) { + Log(debug_out_, "Python package directory not exist at {}, assuming " + "already deleted!\n", py_dir); + return; + } + + std::error_code err; + if (std::filesystem::remove_all(py_dir, err)) { + Log(debug_out_, "success!\n"); + } + else { + wxLogError("Failed to reset virtual environment: %s", err.message()); + Log(debug_out_, "failed!\n"); + } +} + +void Frame::OnClearOSC(wxCommandEvent& event) +{ + std::filesystem::path osc_path = "C:/Users"; + osc_path /= wxGetUserName().ToStdString(); + osc_path /= "AppData/LocalLow/VRChat/vrchat/OSC"; + osc_path = osc_path.lexically_normal(); + Log(debug_out_, "OSC configs are stored at {}\n", osc_path.string()); + + if (!std::filesystem::is_directory(osc_path)) { + Log(debug_out_, "OSC configs do not exist at {}, assuming already " + "deleted!\n", osc_path.string()); + return; + } + + Log(debug_out_, "Deleting OSC configs... "); + std::error_code err; + if (std::filesystem::remove_all(osc_path, err)) { + Log(debug_out_, "success!\n"); + } + else { + wxLogError("Failed to delete OSC configs: %s", err.message()); + Log(debug_out_, "failed!\n"); + } +} + void Frame::OnUnityParamChangeImpl() { int chars_per_sync_idx = unity_chars_per_sync_->GetSelection(); if (chars_per_sync_idx == wxNOT_FOUND) { diff --git a/GUI/GUI/GUI/Frame.h b/GUI/GUI/GUI/Frame.h index 28c8f09..d179728 100644 --- a/GUI/GUI/GUI/Frame.h +++ b/GUI/GUI/GUI/Frame.h @@ -20,9 +20,11 @@ private: wxPanel* main_panel_; wxPanel* transcribe_panel_; wxPanel* unity_panel_; + wxPanel* debug_panel_; wxTextCtrl* transcribe_out_; wxTextCtrl* unity_out_; + wxTextCtrl* debug_out_; wxTextCtrl* unity_animator_generated_dir_; wxTextCtrl* unity_animator_generated_name_; @@ -61,6 +63,7 @@ private: void OnExit(wxCommandEvent& event); void OnNavbarTranscribe(wxCommandEvent& event); void OnNavbarUnity(wxCommandEvent& event); + void OnNavbarDebug(wxCommandEvent& event); void OnSetupPython(wxCommandEvent& event); void OnDumpMics(wxCommandEvent& event); void OnAppStart(wxCommandEvent& event); @@ -70,6 +73,10 @@ private: void OnGenerateFX(wxCommandEvent& event); void OnUnityParamChangeImpl(); void OnUnityParamChange(wxCommandEvent& event); + void OnListPip(wxCommandEvent& event); + void OnClearPip(wxCommandEvent& event); + void OnResetVenv(wxCommandEvent& event); + void OnClearOSC(wxCommandEvent& event); void LoadAndSetIcons(); void Resize(); diff --git a/GUI/GUI/GUI/PythonWrapper.cpp b/GUI/GUI/GUI/PythonWrapper.cpp index 60437d2..2bf1a47 100644 --- a/GUI/GUI/GUI/PythonWrapper.cpp +++ b/GUI/GUI/GUI/PythonWrapper.cpp @@ -117,6 +117,30 @@ bool PythonWrapper::InvokeWithArgs(std::vector&& args, std::move(args), py_stdout, py_stderr); } +bool PythonWrapper::InvokeWithArgs(std::vector&& args, + const std::string&& err_msg, + wxTextCtrl* const out) { + std::string py_stdout, py_stderr; + if (InvokeWithArgs(std::move(args), &py_stdout, &py_stderr)) { + Log(out, "success!\n"); + Log(out, py_stdout.c_str()); + if (!py_stdout.empty()) { + Log(out, "\n"); + } + Log(out, py_stderr.c_str()); + if (!py_stderr.empty()) { + Log(out, "\n"); + } + return true; + } + else { + wxLogError("%s: %s", err_msg, py_stderr.c_str()); + Log(out, "failed!\n"); + return false; + } +} + + std::string PythonWrapper::GetVersion() { std::string py_stdout, py_stderr; bool ok = InvokeWithArgs({ "--version" }, &py_stdout, &py_stderr); @@ -214,28 +238,13 @@ bool PythonWrapper::GenerateAnimator( { Log(out, "Generating shader for {}x{} board (pass 0)...", config.rows, config.cols); - - std::string py_stdout, py_stderr; - if (InvokeWithArgs({ generate_shader_path, + if (!InvokeWithArgs({ generate_shader_path, "--bytes_per_char", std::to_string(config.bytes_per_char), "--rows", std::to_string(config.rows), "--cols", std::to_string(config.cols), "--shader_template", shader_template_path, "--shader_path", shader_path }, - &py_stdout, &py_stderr)) { - Log(out, "success!\n"); - Log(out, py_stdout.c_str()); - if (!py_stdout.empty()) { - Log(out, "\n"); - } - Log(out, py_stderr.c_str()); - if (!py_stderr.empty()) { - Log(out, "\n"); - } - } - else { - wxLogError("Failed to generate shader: %s", py_stderr.c_str()); - Log(out, "failed!\n"); + "Failed to generate shader", out)) { return false; } } @@ -243,26 +252,13 @@ bool PythonWrapper::GenerateAnimator( Log(out, "Generating shader for {}x{} board (pass 1)...", config.rows, config.cols); std::string py_stdout, py_stderr; - if (InvokeWithArgs({ generate_shader_path, + if (!InvokeWithArgs({ generate_shader_path, "--bytes_per_char", std::to_string(config.bytes_per_char), "--rows", std::to_string(config.rows), "--cols", std::to_string(config.cols), "--shader_template", shader_lighting_template_path, "--shader_path", shader_lighting_path }, - &py_stdout, &py_stderr)) { - Log(out, "success!\n"); - Log(out, py_stdout.c_str()); - if (!py_stdout.empty()) { - Log(out, "\n"); - } - Log(out, py_stderr.c_str()); - if (!py_stderr.empty()) { - Log(out, "\n"); - } - } - else { - wxLogError("Failed to generate shader: %s", py_stderr.c_str()); - Log(out, "failed!\n"); + "Failed to generate shader", out)) { return false; } } @@ -328,203 +324,89 @@ bool PythonWrapper::GenerateAnimator( } { Log(out, "Generating guid.map... "); - std::string py_stdout, py_stderr; - if (PythonWrapper::InvokeWithArgs({ libunity_path, "guid_map", + if (!InvokeWithArgs({ libunity_path, "guid_map", "--project_root", Quote(config.assets_path), "--save_to", Quote(guid_map_path), }, - &py_stdout, &py_stderr)) { - Log(out, "success!\n"); - Log(out, py_stdout.c_str()); - if (!py_stdout.empty()) { - Log(out, "\n"); - } - Log(out, py_stderr.c_str()); - if (!py_stderr.empty()) { - Log(out, "\n"); - } - } - else { - wxLogError("Failed to generate guid.map: %s", py_stderr.c_str()); - Log(out, "failed!\n"); + "Failed to generate guid.map", out)) { return false; } } { Log(out, "Generating animations... "); - std::string py_stdout, py_stderr; - if (InvokeWithArgs({ libtastt_path, "gen_anims", + if (!InvokeWithArgs({ libtastt_path, "gen_anims", "--gen_anim_dir", Quote(tastt_animations_path), "--guid_map", Quote(guid_map_path), "--chars_per_sync", std::to_string(config.chars_per_sync), "--bytes_per_char", std::to_string(config.bytes_per_char), "--rows", std::to_string(config.rows), "--cols", std::to_string(config.cols)}, - &py_stdout, &py_stderr)) { - Log(out, "success!\n"); - Log(out, py_stdout.c_str()); - if (!py_stdout.empty()) { - Log(out, "\n"); - } - Log(out, py_stderr.c_str()); - if (!py_stderr.empty()) { - Log(out, "\n"); - } - } - else { - wxLogError("Failed to generate animations: %s", py_stderr.c_str()); - Log(out, "failed!\n"); + "Failed to generate animations", out)) { return false; } } { Log(out, "Generating FX layer... "); - std::string py_stdout, py_stderr; - if (InvokeWithArgs({ libtastt_path, "gen_fx", + if (!InvokeWithArgs({ libtastt_path, "gen_fx", "--fx_dest", Quote(tastt_fx0_path), "--gen_anim_dir", Quote(tastt_animations_path), "--guid_map", Quote(guid_map_path), "--chars_per_sync", std::to_string(config.chars_per_sync), "--bytes_per_char", std::to_string(config.bytes_per_char), "--rows", std::to_string(config.rows), - "--cols", std::to_string(config.cols)}, - &py_stdout, &py_stderr)) { - Log(out, "success!\n"); - Log(out, py_stdout.c_str()); - if (!py_stdout.empty()) { - Log(out, "\n"); - } - Log(out, py_stderr.c_str()); - if (!py_stderr.empty()) { - Log(out, "\n"); - } - } - else { - wxLogError("Failed to generate FX layer: %s", py_stderr.c_str()); - Log(out, "failed!\n"); + "--cols", std::to_string(config.cols) }, + "Failed to generate FX layer", out)) { return false; } } { Log(out, "Adding enable/disable toggle... "); - std::string py_stdout, py_stderr; - if (InvokeWithArgs({ libunity_path, "add_toggle", + if (!InvokeWithArgs({ libunity_path, "add_toggle", "--fx0", Quote(tastt_fx0_path), "--fx_dest", Quote(tastt_fx1_path), "--gen_anim_dir", Quote(tastt_animations_path), "--guid_map", Quote(guid_map_path), }, - &py_stdout, &py_stderr)) { - Log(out, "success!\n"); - Log(out, py_stdout.c_str()); - if (!py_stdout.empty()) { - Log(out, "\n"); - } - Log(out, py_stderr.c_str()); - if (!py_stderr.empty()) { - Log(out, "\n"); - } - } - else { - wxLogError("Failed to add enable/disable toggle: %s", py_stderr.c_str()); - Log(out, "failed!\n"); + "Failed to add enable/disable toggle", out)) { return false; } } { Log(out, "Merging with user animator... "); - std::string py_stdout, py_stderr; - if (InvokeWithArgs({ libunity_path, "merge", + if (!InvokeWithArgs({ libunity_path, "merge", "--fx0", Quote(config.fx_path), "--fx1", Quote(tastt_fx1_path), "--fx_dest", Quote(tastt_fx2_path), }, - &py_stdout, &py_stderr)) { - Log(out, "success!\n"); - Log(out, py_stdout.c_str()); - if (!py_stdout.empty()) { - Log(out, "\n"); - } - Log(out, py_stderr.c_str()); - if (!py_stderr.empty()) { - Log(out, "\n"); - } - } - else { - wxLogError("Failed to merge animators: %s", py_stderr.c_str()); - Log(out, "failed!\n"); + "Failed to merge animators", out)) { return false; } } { Log(out, "Setting noop animations... "); - std::string py_stdout, py_stderr; - if (InvokeWithArgs({ libunity_path, "set_noop_anim", + if (!InvokeWithArgs({ libunity_path, "set_noop_anim", "--fx0", Quote(tastt_fx2_path), "--fx_dest", Quote(tastt_animator_path), "--gen_anim_dir", Quote(tastt_animations_path), "--guid_map", Quote(guid_map_path), }, - &py_stdout, &py_stderr)) { - Log(out, "success!\n"); - Log(out, py_stdout.c_str()); - if (!py_stdout.empty()) { - Log(out, "\n"); - } - Log(out, py_stderr.c_str()); - if (!py_stderr.empty()) { - Log(out, "\n"); - } - } - else { - wxLogError("Failed to set noop animations: %s", py_stderr.c_str()); - Log(out, "failed!\n"); + "Failed to set noop animations", out)) { return false; } } { Log(out, "Generating avatar parameters... "); - std::string py_stdout, py_stderr; - if (InvokeWithArgs({ generate_params_path, + if (!InvokeWithArgs({ generate_params_path, "--old_params", Quote(config.params_path), "--new_params", Quote(tastt_params_path), "--chars_per_sync", std::to_string(config.chars_per_sync), "--bytes_per_char", std::to_string(config.bytes_per_char) }, - &py_stdout, &py_stderr)) { - Log(out, "success!\n"); - Log(out, py_stdout.c_str()); - if (!py_stdout.empty()) { - Log(out, "\n"); - } - Log(out, py_stderr.c_str()); - if (!py_stderr.empty()) { - Log(out, "\n"); - } - } - else { - wxLogError("Failed to generate avatar parameters: %s", py_stderr.c_str()); - Log(out, "failed!\n"); + "Failed to generate avatar parameters", out)) { return false; } } { Log(out, "Generating avatar menu... "); - std::string py_stdout, py_stderr; - // No idea why, but inlining this into `InvokeWithArgs` confuses the compiler. - std::vector args = { generate_menu_path, + if (!InvokeWithArgs({ generate_menu_path, "--old_menu", Quote(config.menu_path), - "--new_menu", Quote(tastt_menu_path), }; - if (InvokeWithArgs( std::move(args), - &py_stdout, &py_stderr)) { - Log(out, "success!\n"); - Log(out, py_stdout.c_str()); - if (!py_stdout.empty()) { - Log(out, "\n"); - } - Log(out, py_stderr.c_str()); - if (!py_stderr.empty()) { - Log(out, "\n"); - } - } - else { - wxLogError("Failed to generate avatar menu: %s", py_stderr.c_str()); - Log(out, "failed!\n"); + "--new_menu", Quote(tastt_menu_path) }, + "Failed to generate avatar menu", out)) { return false; } } diff --git a/GUI/GUI/GUI/PythonWrapper.h b/GUI/GUI/GUI/PythonWrapper.h index c28a1f1..4ae4583 100644 --- a/GUI/GUI/GUI/PythonWrapper.h +++ b/GUI/GUI/GUI/PythonWrapper.h @@ -37,6 +37,9 @@ namespace PythonWrapper bool InvokeWithArgs(std::vector&& args, std::string* py_stdout, std::string* py_stderr = NULL); + bool InvokeWithArgs(std::vector&& args, + const std::string&& err_msg, wxTextCtrl* out); + // Execute python --version. std::string GetVersion(); -- cgit v1.2.3