diff --git a/backend/include/backend/main.hpp b/backend/include/backend/main.hpp index 3476fcf1..2b797b95 100644 --- a/backend/include/backend/main.hpp +++ b/backend/include/backend/main.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -24,7 +25,7 @@ class Main { public: - Main(int const argc, char const* const* argv); + Main(ProgramOptions options); ~Main(); Main(Main const&) = delete; diff --git a/backend/include/backend/program_options.hpp b/backend/include/backend/program_options.hpp new file mode 100644 index 00000000..705cfaba --- /dev/null +++ b/backend/include/backend/program_options.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include + +struct ProgramOptions +{ + bool enableDevTools = false; +}; + +std::optional parseProgramOptions(int argc, char const* const* argv); \ No newline at end of file diff --git a/backend/source/backend/CMakeLists.txt b/backend/source/backend/CMakeLists.txt index 7f63ebab..be930835 100644 --- a/backend/source/backend/CMakeLists.txt +++ b/backend/source/backend/CMakeLists.txt @@ -6,6 +6,7 @@ add_library( rpc_filesystem.cpp rpc_system.cpp theme_finder.cpp + program_options.cpp password/password_prompter.cpp process/process.cpp process/process_store.cpp @@ -45,7 +46,7 @@ target_sources( main.cpp ) -find_package(Boost CONFIG 1.86.0 REQUIRED COMPONENTS filesystem process) +find_package(Boost CONFIG 1.86.0 REQUIRED COMPONENTS filesystem process program_options) target_include_directories(backend PUBLIC "${CMAKE_SOURCE_DIR}/backend/include") @@ -65,6 +66,7 @@ target_link_libraries( efsw-static Boost::filesystem Boost::process + Boost::program_options roar-include-only shared-data utility @@ -92,6 +94,9 @@ else() set(BROWSER_ENGINE "webkitgtk") endif() +include(ProcessorCount) +ProcessorCount(nproc) + if (NOT OMIT_FRONTEND_BUILD) # Creates a target that is compiled through emscripten. This target becomes the frontend part. nui_add_emscripten_target( @@ -119,6 +124,8 @@ if (NOT OMIT_FRONTEND_BUILD) # -DXML_TO_NUI_TOOL=$ -DOFFLINE_BUILD=${OFFLINE_BUILD} -DBACKEND_BUILD_TYPE=${BACKEND_BUILD_TYPE} + BUILD_OPTIONS + -- -j${nproc} ) target_sources( diff --git a/backend/source/backend/linux/main_linux.cpp b/backend/source/backend/linux/main_linux.cpp index 4e77b5d7..f0fcf1e8 100644 --- a/backend/source/backend/linux/main_linux.cpp +++ b/backend/source/backend/linux/main_linux.cpp @@ -1,6 +1,75 @@ // Nothing yet #include +#include -Main::PlatformSpecifics::PlatformSpecifics(Nui::Window&, Nui::RpcHub&) -{} \ No newline at end of file +#if GTK_MAJOR_VERSION >= 4 +# include +# include +# ifdef GDK_WINDOWING_X11 +# include +# endif +#elif GTK_MAJOR_VERSION >= 3 +# include +# include +#endif + +namespace +{ + bool isFilteredAction(WebKitContextMenuAction action) + { + switch (action) + { + case WEBKIT_CONTEXT_MENU_ACTION_RELOAD: + case WEBKIT_CONTEXT_MENU_ACTION_GO_BACK: + case WEBKIT_CONTEXT_MENU_ACTION_GO_FORWARD: + case WEBKIT_CONTEXT_MENU_ACTION_STOP: + return true; + default: + return false; + } + } +} + +gboolean on_context_menu( + WebKitWebView* /*web_view*/, + WebKitContextMenu* context_menu, + GdkEvent* /*event*/, + WebKitHitTestResult* /*hit_test_result*/, + gpointer /*user_data*/ +) +{ + GList* items = webkit_context_menu_get_items(context_menu); + GList* copy = g_list_copy(items); + for (GList* l = copy; l != NULL; l = l->next) + { + auto* item = static_cast(l->data); + if (isFilteredAction(webkit_context_menu_item_get_stock_action(item))) + webkit_context_menu_remove(context_menu, item); + } + g_list_free(copy); + return FALSE; // show modified menu +} + +Main::PlatformSpecifics::PlatformSpecifics(Nui::Window& wnd, Nui::RpcHub&) +{ + auto* webview = static_cast(wnd.getNativeWebView()); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wcast-function-type-strict" + g_signal_connect( + webview, + "context-menu", + G_CALLBACK( + +[](WebKitWebView* web_view, + WebKitContextMenu* context_menu, + GdkEvent* event, + WebKitHitTestResult* hit_test_result, + gpointer user_data) -> gboolean + { + return on_context_menu(web_view, context_menu, event, hit_test_result, user_data); + } + ), + nullptr + ); +#pragma clang diagnostic pop +} \ No newline at end of file diff --git a/backend/source/backend/main.cpp b/backend/source/backend/main.cpp index f2a65680..01e89c81 100644 --- a/backend/source/backend/main.cpp +++ b/backend/source/backend/main.cpp @@ -7,6 +7,7 @@ #endif #include +#include #include #include @@ -174,7 +175,7 @@ Main::LoggerSetup::LoggerSetup(Persistence::StateHolder& stateHolder) ); } -Main::Main(int const, char const* const*) +Main::Main(ProgramOptions options) : shuttingDown_{false} , programDir_{boost::dll::program_location().parent_path().string()} , stateHolder_{programDir_} @@ -183,7 +184,7 @@ Main::Main(int const, char const* const*) Nui::WindowOptions{ .title = "NuiSftp"s, #ifdef NDEBUG - .debug = false, + .debug = options.enableDevTools, #else .debug = true, #endif @@ -346,6 +347,10 @@ void Main::startChildSignalTimer() int main(int const argc, char const* const* argv) { + auto options = parseProgramOptions(argc, argv); + if (!options) + return 0; + #ifdef __linux__ # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wc99-designator" @@ -380,7 +385,7 @@ int main(int const argc, char const* const* argv) ssh_init(); { - Main m{argc, argv}; + Main m{std::move(*options)}; m.startChildSignalTimer(); m.show(); } diff --git a/backend/source/backend/program_options.cpp b/backend/source/backend/program_options.cpp new file mode 100644 index 00000000..5d1db501 --- /dev/null +++ b/backend/source/backend/program_options.cpp @@ -0,0 +1,37 @@ +#include + +#include + +#include + +std::optional parseProgramOptions(int argc, char const* const* argv) +{ + try + { + boost::program_options::options_description desc("Allowed options"); + desc.add_options()("help", "produce help message")( + "enable-dev-tools", "Enable developer tools (DevTools in Browser)" + ); + + boost::program_options::variables_map vm; + boost::program_options::store(boost::program_options::parse_command_line(argc, argv, desc), vm); + boost::program_options::notify(vm); + + if (vm.count("help")) + { + std::cout << desc << "\n"; + return std::nullopt; + } + + ProgramOptions options; + if (vm.count("enable-dev-tools")) + options.enableDevTools = true; + + return options; + } + catch (std::exception& e) + { + std::cerr << "Error parsing program options: " << e.what() << "\n"; + return std::nullopt; + } +} \ No newline at end of file diff --git a/backend/source/backend/theme_finder.cpp b/backend/source/backend/theme_finder.cpp index 2a8a786d..8ed35c55 100644 --- a/backend/source/backend/theme_finder.cpp +++ b/backend/source/backend/theme_finder.cpp @@ -9,34 +9,32 @@ ThemeFinder::ThemeFinder(std::filesystem::path const& relativeRoot, AppWideEvent events.onReloadThemes, [&events, relativeRoot](bool) { + Log::info("Reloading themes... relativeRoot='{}'", relativeRoot.generic_string()); events.availableThemes = findAvailableThemes(relativeRoot); + Log::info("availableThemes updated, count={}", events.availableThemes.value().size()); events.availableThemes.eventContext().sync(); + Log::info("availableThemes sync done."); } )} -{} +{ + Log::info("ThemeFinder constructed with relativeRoot='{}'", relativeRoot.generic_string()); +} std::vector ThemeFinder::findAvailableThemes(std::filesystem::path const& relativeRoot) { - const auto dirs = getThemeDirs(relativeRoot); - std::vector themes{}; - Log::info("Finding available themes in {} candidate directories.", dirs.size()); - for (const auto& dir : dirs) - { - Log::info("Checking theme directory: {}", dir.generic_string()); - if (!std::filesystem::exists(dir) || !std::filesystem::is_directory(dir)) - { - Log::info("Skipping non-existing or non-directory theme path: {}", dir.generic_string()); - continue; - } + Log::info("findAvailableThemes called with relativeRoot='{}'", relativeRoot.generic_string()); + std::error_code ec; + const bool rootExists = std::filesystem::exists(relativeRoot, ec); + Log::info("relativeRoot exists={}, ec='{}'", rootExists, ec.message()); - for (const auto& entry : std::filesystem::directory_iterator(dir)) - { - if (entry.is_regular_file() && entry.path().extension() == ".css") - { - Log::info("Found theme file: {}", entry.path().generic_string()); - themes.push_back(entry.path().stem()); - } - } + const auto files = findFilesInSearchPaths(relativeRoot, "themes/*.css"); + Log::info("findFilesInSearchPaths returned {} file(s).", files.size()); + std::vector themes; + themes.reserve(files.size()); + for (const auto& file : files) + { + Log::info("Found theme file: '{}', stem='{}'", file.generic_string(), file.stem().generic_string()); + themes.push_back(file.stem()); } Log::info("Theme discovery complete, found {} themes.", themes.size()); return themes; diff --git a/frontend/include/frontend/settings/atomic_setting/combo_setting.hpp b/frontend/include/frontend/settings/atomic_setting/combo_setting.hpp index d27694f4..3a4da050 100644 --- a/frontend/include/frontend/settings/atomic_setting/combo_setting.hpp +++ b/frontend/include/frontend/settings/atomic_setting/combo_setting.hpp @@ -164,7 +164,10 @@ class ComboSetting : public Setting .onOpen = [this]() { if (doLoad_) + { + Log::info("ComboSetting doing load on open."); return doLoad_(); + } return false; }, .dontUpdateValue = true, diff --git a/frontend/source/frontend/main.cpp b/frontend/source/frontend/main.cpp index ce6707e9..566d8694 100644 --- a/frontend/source/frontend/main.cpp +++ b/frontend/source/frontend/main.cpp @@ -23,6 +23,20 @@ static std::unique_ptr themeController{}; static std::unique_ptr mainPage{}; static std::unique_ptr dom{}; +namespace +{ + void printKeys(Nui::val obj) + { + Nui::val keys = Nui::val::global("Object").call("keys", obj); + int length = keys["length"].as(); + for (int i = 0; i < length; ++i) + { + std::string key = keys[i].as(); + Log::debug("Key: '{}'", key); + } + } +} + bool tryLoad(std::shared_ptr const& setupWait) { static int counter = 0; @@ -85,6 +99,16 @@ bool tryLoad(std::shared_ptr const& setupWait) Log::info("Calling setup completion function"); mainPage->onSetupComplete(); + + Log::debug("Dumping nui_rpc.backend and nui_rpc.frontend for debugging:"); + auto nuiRpc = Nui::val::global("nui_rpc"); + if (!nuiRpc.isUndefined() && nuiRpc.hasOwnProperty("backend")) + { + Log::debug("nui_rpc.backend:"); + printKeys(nuiRpc["backend"]); + Log::debug("nui_rpc.frontend:"); + printKeys(nuiRpc["frontend"]); + } } catch (std::exception const& exc) { diff --git a/persistence/include/persistence/state_core.hpp b/persistence/include/persistence/state_core.hpp index 66b2b7ce..f482021a 100644 --- a/persistence/include/persistence/state_core.hpp +++ b/persistence/include/persistence/state_core.hpp @@ -116,11 +116,6 @@ namespace Persistence { if constexpr (CanCallUseDefaultsFrom>) { - Log::debug( - "Calling useDefaultsFrom for member {} of type {}.", - memAccessor.name, - typeid(decltype(toFill.*memAccessor.pointer)).name() - ); useDefaultsFrom(toFill.*memAccessor.pointer, defaultsFromThis.*memAccessor.pointer); } else diff --git a/persistence/source/persistence/state_holder_backend.cpp b/persistence/source/persistence/state_holder_backend.cpp index d7fd6bd4..cac7e8d6 100644 --- a/persistence/source/persistence/state_holder_backend.cpp +++ b/persistence/source/persistence/state_holder_backend.cpp @@ -480,49 +480,32 @@ namespace Persistence void StateHolder::loadLanguageFiles(std::function const&)> const& onLoadComplete) { - const auto assetsDir = getAssetsDirectory(programDirectory_); - if (!assetsDir) + const auto files = findFilesInSearchPaths(programDirectory_.parent_path(), "assets/languages/*.yaml"); + if (files.empty()) { - Log::error("Assets directory not found, cannot load language files."); + Log::error("No language files found in any search path."); onLoadComplete(std::nullopt); return; } - const auto fileDirectory = *assetsDir / "languages"; - std::filesystem::directory_iterator dirIt; - try - { - dirIt = std::filesystem::directory_iterator(fileDirectory); - } - catch (std::exception const& e) - { - Log::error("Failed to read language files directory: {}", e.what()); - onLoadComplete(std::nullopt); - return; - } nlohmann::json assembled = nlohmann::json::object(); - for (const auto& entry : dirIt) + for (const auto& file : files) { - if (entry.is_regular_file() && entry.path().extension() == ".yaml") - { - std::optional content; - loadLanguageFile( - entry.path(), - [&content](const auto maybeJson) - { - content = maybeJson; - } - ); - if (!content) + std::optional content; + loadLanguageFile( + file, + [&content](const auto maybeJson) { - Log::error("Failed to load language file: {}", entry.path().string()); - onLoadComplete(std::nullopt); - return; + content = maybeJson; } - - const auto fileName = entry.path().stem().string(); - assembled[fileName] = *content; + ); + if (!content) + { + Log::error("Failed to load language file: {}", file.string()); + onLoadComplete(std::nullopt); + return; } + assembled[file.stem().string()] = *content; } onLoadComplete(std::move(assembled)); } diff --git a/scripts/copy_exe_to_opt.sh b/scripts/copy_exe_to_opt.sh new file mode 100755 index 00000000..c69b7b79 --- /dev/null +++ b/scripts/copy_exe_to_opt.sh @@ -0,0 +1,8 @@ +#!/bin/env bash + +sudo rm -f /opt/nui-sftp/bin/nui-sftp +sudo cp -f ./build/clang_release/bin/nui-sftp /opt/nui-sftp/bin/nui-sftp + +sudo rm -rf /opt/nui-sftp/frontend +sudo mkdir -p /opt/nui-sftp/frontend +sudo cp -rf ./build/clang_release/module_nui-sftp/bin/. /opt/nui-sftp/frontend \ No newline at end of file diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 9936992c..609cf39b 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -51,6 +51,10 @@ mkdir -p "${INSTALL_TARGET}/themes" mkdir -p "${INSTALL_TARGET}/assets/icons" cp "${EXECUTABLE}" "${INSTALL_TARGET}/bin/${EXECUTABLE_NAME}" +if [ "$IS_WINDOWS" = true ]; then + # use ldd to copy dependencies from msys2 into the bin dir + ldd "${EXECUTABLE}" | grep "clang" | awk 'NF == 4 { system("cp " $3 " '"${INSTALL_TARGET}/bin/"'") }' +fi # Assets (Images, icons, language files)... cp -r "${SOURCE_DIRECTORY}/static/assets/." "${INSTALL_TARGET}/assets" @@ -62,7 +66,20 @@ if [ "$OMIT_FRONTEND" = false ]; then cp -r "${BUILD_DIRECTORY}/module_nui-sftp/bin/." "${INSTALL_TARGET}/frontend" fi -# Dont create a symlink on windows or if NOLINK is true -if [ "$IS_WINDOWS" = false ] && [ "$NOLINK" = false ]; then - ln -s "./bin/${EXECUTABLE_NAME}" "${INSTALL_TARGET}/${EXECUTABLE_NAME}" +if [ "$NOLINK" = false ]; then + if [ "$IS_WINDOWS" = false ]; then + # Dont create a symlink on windows or if NOLINK is true + ln -s "./bin/${EXECUTABLE_NAME}" "${INSTALL_TARGET}/${EXECUTABLE_NAME}" + else + # Convert to Windows path + WIN_INSTALL_TARGET=$(cygpath -w "${INSTALL_TARGET}") + + powershell -Command " + \$WshShell = New-Object -ComObject WScript.Shell; + \$Shortcut = \$WshShell.CreateShortcut('${WIN_INSTALL_TARGET}\\${EXECUTABLE_NAME}.lnk'); + \$Shortcut.TargetPath = '.\\bin\\${EXECUTABLE_NAME}'; + \$Shortcut.WorkingDirectory = '${WIN_INSTALL_TARGET}'; + \$Shortcut.Save(); + " + fi fi diff --git a/utility/include/utility/resources.hpp b/utility/include/utility/resources.hpp index 931241dd..6c205022 100644 --- a/utility/include/utility/resources.hpp +++ b/utility/include/utility/resources.hpp @@ -29,6 +29,35 @@ searchAllInPaths(std::filesystem::path const& relativeRoot, std::filesystem::pat */ std::vector getThemeDirs(std::filesystem::path const& relativeRoot); +/** + * @brief Finds all regular files within baseDir matching a relative path pattern with fnmatch-style + * wildcards (* ?) applied segment by segment. Returns paths relative to baseDir. + */ +std::vector +matchPatternInDir(std::filesystem::path const& baseDir, std::filesystem::path const& relativePattern); + +/** + * @brief Finds all regular files matching a relative path pattern across multiple search paths. + * Supports fnmatch-style wildcards (* ?) on individual path segments. + * Deduplicates by relative path: when the same relative path is found in multiple search paths, + * only the match from the earliest search path is kept (lower index = higher priority). + * Returns absolute paths. + */ +std::vector +findFilesInSearchPaths( + std::vector const& searchPaths, + std::filesystem::path const& relativePattern +); + +/** + * @brief Overload that derives search paths from relativeRoot using the standard search path list. + */ +std::vector +findFilesInSearchPaths( + std::filesystem::path const& relativeRoot, + std::filesystem::path const& relativePattern +); + std::optional mapUrlToFile(std::filesystem::path const& programDir, std::string const& urlPathString); diff --git a/utility/source/utility/resources.cpp b/utility/source/utility/resources.cpp index 66ae8dfa..df936ec1 100644 --- a/utility/source/utility/resources.cpp +++ b/utility/source/utility/resources.cpp @@ -6,6 +6,7 @@ #endif #include +#include namespace { @@ -14,6 +15,69 @@ namespace return pathString.size() >= ending.size() && pathString.substr(pathString.size() - ending.size()) == ending; }; + bool segmentMatchesPattern(std::string_view name, std::string_view pattern) + { + if (pattern.empty()) + return name.empty(); + if (pattern[0] == '*') + { + for (std::size_t i = 0; i <= name.size(); ++i) + if (segmentMatchesPattern(name.substr(i), pattern.substr(1))) + return true; + return false; + } + if (name.empty()) + return false; + if (pattern[0] == '?') + return segmentMatchesPattern(name.substr(1), pattern.substr(1)); + if (pattern[0] != name[0]) + return false; + return segmentMatchesPattern(name.substr(1), pattern.substr(1)); + } + + bool hasGlobChars(std::string_view s) + { + return s.find_first_of("*?") != std::string_view::npos; + } + + void matchPatternInDirImpl( + std::filesystem::path const& currentDir, + std::vector const& segments, + std::size_t index, + std::filesystem::path const& relative, + std::vector& results + ) + { + if (index == segments.size()) + { + std::error_code ec; + if (std::filesystem::is_regular_file(currentDir, ec)) + results.push_back(relative); + return; + } + + const auto& seg = segments[index]; + if (!hasGlobChars(seg)) + { + const auto next = currentDir / seg; + std::error_code ec; + if (std::filesystem::exists(next, ec)) + matchPatternInDirImpl(next, segments, index + 1, relative / seg, results); + } + else + { + std::error_code ec; + if (!std::filesystem::is_directory(currentDir, ec)) + return; + for (const auto& entry : std::filesystem::directory_iterator(currentDir, ec)) + { + const auto name = entry.path().filename().string(); + if (segmentMatchesPattern(name, seg)) + matchPatternInDirImpl(entry.path(), segments, index + 1, relative / name, results); + } + } + } + std::vector getSearchPath(std::filesystem::path const& relativeRoot) { std::vector searchPaths; @@ -105,6 +169,46 @@ std::vector getThemeDirs(std::filesystem::path const& rel return transformedSearchPaths(relativeRoot, "themes"); } +std::vector +matchPatternInDir(std::filesystem::path const& baseDir, std::filesystem::path const& relativePattern) +{ + std::vector segments; + for (auto const& part : relativePattern) + { + const auto s = part.string(); + if (!s.empty() && s != "." && s != "/" && s != "\\") + segments.push_back(s); + } + std::vector results; + matchPatternInDirImpl(baseDir, segments, 0, {}, results); + return results; +} + +std::vector +findFilesInSearchPaths( + std::vector const& searchPaths, + std::filesystem::path const& relativePattern +) +{ + std::unordered_set seen; + std::vector result; + for (const auto& searchPath : searchPaths) + { + for (auto const& relPath : matchPatternInDir(searchPath, relativePattern)) + { + if (seen.insert(relPath.generic_string()).second) + result.push_back(searchPath / relPath); + } + } + return result; +} + +std::vector +findFilesInSearchPaths(std::filesystem::path const& relativeRoot, std::filesystem::path const& relativePattern) +{ + return findFilesInSearchPaths(getSearchPath(relativeRoot), relativePattern); +} + std::optional mapUrlToFile(std::filesystem::path const& resourceDir, std::string const& urlPathString) { @@ -112,6 +216,10 @@ mapUrlToFile(std::filesystem::path const& resourceDir, std::string const& urlPat { return endsWithImpl(urlPathString, ending); }; + auto firstOf = [](std::vector const& v) -> std::optional + { + return v.empty() ? std::nullopt : std::optional{v[0]}; + }; const auto path = [&]() -> std::optional { @@ -119,19 +227,17 @@ mapUrlToFile(std::filesystem::path const& resourceDir, std::string const& urlPat const auto relative = std::filesystem::relative(urlPathString, "/"); if (endsWith(".css") && relative.parent_path().filename() == "themes") - { - return searchInPaths(getThemeDirs(resourceDir), relative.filename()); - } + return firstOf(findFilesInSearchPaths(getThemeDirs(resourceDir), relative.filename())); if (endsWith(".js") || endsWith(".map") || endsWith(".css") || endsWith(".ttf") || endsWith(".html")) { #ifndef NDEBUG - return searchInPaths(transformedSearchPaths(resourceDir, "module_nui-sftp/bin"), relative); + return firstOf(findFilesInSearchPaths(transformedSearchPaths(resourceDir, "module_nui-sftp/bin"), relative)); #endif - return searchInPaths(transformedSearchPaths(resourceDir, "frontend"), relative); + return firstOf(findFilesInSearchPaths(transformedSearchPaths(resourceDir, "frontend"), relative)); } else - return searchInPaths(transformedSearchPaths(resourceDir, "assets"), relative); + return firstOf(findFilesInSearchPaths(transformedSearchPaths(resourceDir, "assets"), relative)); return std::nullopt; }(); diff --git a/work_dependencies.json b/work_dependencies.json index 9c1949cf..2e731d3c 100644 --- a/work_dependencies.json +++ b/work_dependencies.json @@ -8,7 +8,7 @@ }, { "url": "https://github.com/NuiCpp/Nui.git", - "rev": "v3.2.1", + "rev": "v3.3.0", "branch": "main", "name": "Nui" },