From 7ee008e150bc5bcf76082d726f719ee0fdfda982 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Wed, 11 Feb 2026 02:37:39 -0600 Subject: Fluorine Manager: full Linux port of Mod Organizer 2 Complete native Linux port with FUSE-based virtual filesystem, Proton/umu-run integration, and Flatpak packaging. Key features: - FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak) - Proton/GE-Proton/umu-run launcher with env var forwarding - Flatpak support (sandbox-aware VFS, NXM handler, umu-run) - Wine prefix management UI - Case-insensitive path resolution for Linux filesystems - QSettings-safe INI handling (avoids Bethesda INI corruption) - Portable instance support with auto-generated launcher scripts Co-Authored-By: Claude Opus 4.6 --- .../src/mobase/wrappers/pyfiletree.cpp | 329 +++++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 libs/plugin_python/src/mobase/wrappers/pyfiletree.cpp (limited to 'libs/plugin_python/src/mobase/wrappers/pyfiletree.cpp') diff --git a/libs/plugin_python/src/mobase/wrappers/pyfiletree.cpp b/libs/plugin_python/src/mobase/wrappers/pyfiletree.cpp new file mode 100644 index 0000000..6dacf26 --- /dev/null +++ b/libs/plugin_python/src/mobase/wrappers/pyfiletree.cpp @@ -0,0 +1,329 @@ +#include "pyfiletree.h" + +#include +#include + +#include "../pybind11_all.h" + +#include +#include + +namespace py = pybind11; +using namespace MOBase; + +namespace mo2::detail { + + // filetree implementation for testing purpose + // + class PyFileTree : public IFileTree { + public: + using callback_t = std::function; + + PyFileTree(std::shared_ptr parent, QString name, + callback_t callback) + : FileTreeEntry(parent, name), IFileTree(), m_Callback(callback) + { + } + + std::shared_ptr addFile(QString name, bool) override + { + if (m_Callback && !m_Callback(name, false)) { + throw UnsupportedOperationException("File rejected by callback."); + } + return IFileTree::addFile(name); + } + + std::shared_ptr addDirectory(QString name) override + { + if (m_Callback && !m_Callback(name, true)) { + throw UnsupportedOperationException("Directory rejected by callback."); + } + return IFileTree::addDirectory(name); + } + + protected: + std::shared_ptr + makeDirectory(std::shared_ptr parent, + QString name) const override + { + return std::make_shared(parent, name, m_Callback); + } + + bool doPopulate([[maybe_unused]] std::shared_ptr parent, + std::vector>&) const override + { + return true; + } + std::shared_ptr doClone() const override + { + return std::make_shared(nullptr, name(), m_Callback); + } + + private: + callback_t m_Callback; + }; + +} // namespace mo2::detail + +#pragma optimize("", off) + +namespace pybind11 { + const void* polymorphic_type_hook::get(const FileTreeEntry* src, + const std::type_info*& type) + { + if (auto p = dynamic_cast(src)) { + type = &typeid(IFileTree); + return p; + } + return src; + } +} // namespace pybind11 + +namespace mo2::python { + + void add_ifiletree_bindings(pybind11::module_& m) + { + // FileTreeEntry class: + auto fileTreeEntryClass = + py::class_>(m, + "FileTreeEntry"); + + // IFileTree class: + auto iFileTreeClass = + py::class_>( + m, "IFileTree", py::multiple_inheritance()); + + // this is FILE_OR_DIRECTORY but as a FileType since we kind of cheat for the + // exposure in Python and this help pybind11 creates proper typing + + const auto FILE_OR_DIRECTORY = static_cast( + FileTreeEntry::FILE_OR_DIRECTORY.toInt()); + + // we do not use the enum directly, we will mostly bind the FileTypes + // (with an S) + py::enum_(fileTreeEntryClass, "FileTypes", + py::arithmetic{}) + .value("FILE", FileTreeEntry::FileType::FILE) + .value("DIRECTORY", FileTreeEntry::FileType::DIRECTORY) + .value("FILE_OR_DIRECTORY", FILE_OR_DIRECTORY) + .export_values(); + + fileTreeEntryClass + + .def("isFile", &FileTreeEntry::isFile) + .def("isDir", &FileTreeEntry::isDir) + .def("fileType", &FileTreeEntry::fileType) + .def("name", &FileTreeEntry::name) + .def("suffix", &FileTreeEntry::suffix) + .def( + "hasSuffix", + [](FileTreeEntry* entry, QStringList suffixes) { + return entry->hasSuffix(suffixes); + }, + py::arg("suffixes")) + .def( + "hasSuffix", + [](FileTreeEntry* entry, QString suffix) { + return entry->hasSuffix(suffix); + }, + py::arg("suffix")) + .def("parent", py::overload_cast<>(&FileTreeEntry::parent)) + .def("path", &FileTreeEntry::path, py::arg("sep") = "\\") + .def("pathFrom", &FileTreeEntry::pathFrom, py::arg("tree"), + py::arg("sep") = "\\") + + // Mutable operation: + .def("detach", &FileTreeEntry::detach) + .def("moveTo", &FileTreeEntry::moveTo, py::arg("tree")) + + // Special methods: + .def("__eq__", + [](const FileTreeEntry* entry, QString other) { + return entry->compare(other) == 0; + }) + .def("__eq__", + [](const FileTreeEntry* entry, std::shared_ptr other) { + return entry == other.get(); + }) + + // Special methods for debug: + .def("__repr__", [](const FileTreeEntry* entry) { + return "FileTreeEntry(\"" + entry->name() + "\")"; + }); + + py::enum_(iFileTreeClass, "InsertPolicy") + .value("FAIL_IF_EXISTS", IFileTree::InsertPolicy::FAIL_IF_EXISTS) + .value("REPLACE", IFileTree::InsertPolicy::REPLACE) + .value("MERGE", IFileTree::InsertPolicy::MERGE) + .export_values(); + + py::enum_(iFileTreeClass, "WalkReturn") + .value("CONTINUE", IFileTree::WalkReturn::CONTINUE) + .value("STOP", IFileTree::WalkReturn::STOP) + .value("SKIP", IFileTree::WalkReturn::SKIP) + .export_values(); + + // in C++ this is not an inner enum due to the conditional feature of glob(), + // but in Python this makes more sense as a inner enum + py::enum_(iFileTreeClass, "GlobPatternType") + .value("GLOB", GlobPatternType::GLOB) + .value("REGEX", GlobPatternType::REGEX) + .export_values(); + + // Non-mutable operations: + iFileTreeClass.def("exists", + py::overload_cast( + &IFileTree::exists, py::const_), + py::arg("path"), py::arg("type") = FILE_OR_DIRECTORY); + iFileTreeClass.def( + "find", py::overload_cast(&IFileTree::find), + py::arg("path"), py::arg("type") = FILE_OR_DIRECTORY); + iFileTreeClass.def("pathTo", &IFileTree::pathTo, py::arg("entry"), + py::arg("sep") = "\\"); + + iFileTreeClass.def( + "walk", + py::overload_cast< + std::function)>, + QString>(&IFileTree::walk, py::const_), + py::arg("callback"), py::arg("sep") = "\\"); + + // the walk() and glob() generator version are free functions in C++ due to the + // conditional nature, but in Python, it makes more sense to have them as method + // of IFileTree directly + + iFileTreeClass.def("walk", [](std::shared_ptr tree) { + return make_generator(walk(tree)); + }); + + iFileTreeClass.def( + "glob", + [](std::shared_ptr tree, QString pattern, + GlobPatternType patternType) { + return make_generator(glob(tree, pattern, patternType)); + }, + py::arg("pattern"), py::arg("type") = GlobPatternType::GLOB); + + // Kind-of-static operations: + iFileTreeClass.def("createOrphanTree", &IFileTree::createOrphanTree, + py::arg("name") = ""); + + // addFile() and addDirectory throws exception instead of returning null + // pointer in order to have better traces. + iFileTreeClass.def( + "addFile", + [](IFileTree* w, QString path, bool replaceIfExists) { + auto result = w->addFile(path, replaceIfExists); + if (result == nullptr) { + throw std::logic_error("addFile failed"); + } + return result; + }, + py::arg("path"), py::arg("replace_if_exists") = false); + iFileTreeClass.def( + "addDirectory", + [](IFileTree* w, QString path) { + auto result = w->addDirectory(path); + if (result == nullptr) { + throw std::logic_error("addDirectory failed"); + } + return result; + }, + py::arg("path")); + + // Merge needs custom return types depending if the user wants overrides + // or not. A failure is translated into an exception for easier tracing + // and handling. + iFileTreeClass.def( + "merge", + [](IFileTree* p, std::shared_ptr other, bool returnOverwrites) + -> std::variant { + IFileTree::OverwritesType overwrites; + auto result = p->merge(other, returnOverwrites ? &overwrites : nullptr); + if (result == IFileTree::MERGE_FAILED) { + throw std::logic_error("merge failed"); + } + if (returnOverwrites) { + return {overwrites}; + } + return {result}; + }, + py::arg("other"), py::arg("overwrites") = false); + + // Insert and erase returns an iterator, which makes no sense in python, + // so we convert it to bool. Erase is also renamed "remove" since + // "erase" is very C++. + iFileTreeClass.def( + "insert", + [](IFileTree* p, std::shared_ptr entry, + IFileTree::InsertPolicy insertPolicy) { + return p->insert(entry, insertPolicy) == p->end(); + }, + py::arg("entry"), + py::arg("policy") = IFileTree::InsertPolicy::FAIL_IF_EXISTS); + + iFileTreeClass.def( + "remove", + [](IFileTree* p, QString name) { + return p->erase(name).first != p->end(); + }, + py::arg("name")); + iFileTreeClass.def( + "remove", + [](IFileTree* p, std::shared_ptr entry) { + return p->erase(entry) != p->end(); + }, + py::arg("entry")); + + iFileTreeClass.def("move", &IFileTree::move, py::arg("entry"), py::arg("path"), + py::arg("policy") = IFileTree::InsertPolicy::FAIL_IF_EXISTS); + iFileTreeClass.def( + "copy", + [](IFileTree* w, std::shared_ptr entry, QString path, + IFileTree::InsertPolicy insertPolicy) { + auto result = w->copy(entry, path, insertPolicy); + if (result == nullptr) { + throw std::logic_error("copy failed"); + } + return result; + }, + py::arg("entry"), py::arg("path") = "", + py::arg("insert_policy") = IFileTree::InsertPolicy::FAIL_IF_EXISTS); + + iFileTreeClass.def("clear", &IFileTree::clear); + iFileTreeClass.def("removeAll", &IFileTree::removeAll, py::arg("names")); + iFileTreeClass.def("removeIf", &IFileTree::removeIf, py::arg("filter")); + + // Special methods: + iFileTreeClass.def("__getitem__", + py::overload_cast(&IFileTree::at)); + + iFileTreeClass.def("__iter__", [](IFileTree* tree) { + return py::make_iterator(*tree); + }); + iFileTreeClass.def("__len__", &IFileTree::size); + iFileTreeClass.def( + "__bool__", +[](const IFileTree* tree) { + return !tree->empty(); + }); + iFileTreeClass.def( + "__repr__", +[](const IFileTree* entry) { + return "IFileTree(\"" + entry->name() + "\")"; + }); + } + + void add_make_tree_function(pybind11::module_& m) + { + m.def( + "makeTree", + [](mo2::detail::PyFileTree::callback_t callback) + -> std::shared_ptr { + return std::make_shared(nullptr, "", callback); + }, + py::arg("callback") = mo2::detail::PyFileTree::callback_t{}); + } + +} // namespace mo2::python + +#pragma optimize("", on) -- cgit v1.3.1