code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include <lz4.h>
#include <openssl/crypto.h>
#include <filesystem>
#include <boost/program_options.hpp>
#include <rotor/asio.hpp>
#include <rotor/thread.hpp>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <exception>

#include "syncspirit-config.h"
#include "constants.h"
#include "config/utils.h"
#include "utils/io.h"
#include "utils/location.h"
#include "utils/log-setup.h"
#include "utils/platform.h"
#include "net/net_supervisor.h"
#include "fs/fs_supervisor.h"
#include "hasher/hasher_supervisor.h"
#include "command.h"
#include "governor_actor.h"

#if defined(__unix__)
#include <signal.h>
#endif

#if defined(__linux__)
#include <pthread.h>
#endif

#ifdef _WIN32
#include <windows.h>
#include <winnls.h>
#endif

namespace bfs = std::filesystem;
namespace po = boost::program_options;
namespace pt = boost::posix_time;
namespace r = rotor;
namespace ra = r::asio;
namespace rth = r::thread;
namespace asio = boost::asio;

using namespace syncspirit;
using namespace syncspirit::daemon;

[[noreturn]] static void report_error_and_die(r::actor_base_t *actor, const r::extended_error_ptr_t &ec) noexcept {
    auto name = actor ? actor->get_identity() : "unknown";
    spdlog::critical("actor '{}' error: {}", name, ec->message());
    std::terminate();
}

struct asio_sys_context_t : ra::system_context_asio_t {
    using parent_t = ra::system_context_asio_t;
    using parent_t::parent_t;
    void on_error(r::actor_base_t *actor, const r::extended_error_ptr_t &ec) noexcept override {
        report_error_and_die(actor, ec);
    }
};

struct thread_sys_context_t : rth::system_context_thread_t {
    using parent_t = rth::system_context_thread_t;
    using parent_t::parent_t;
    void on_error(r::actor_base_t *actor, const r::extended_error_ptr_t &ec) noexcept override {
        report_error_and_die(actor, ec);
    }
};

static std::atomic_bool shutdown_flag = false;

#ifdef _WIN32
BOOL WINAPI consoleHandler(DWORD signal) {
    if (signal == CTRL_C_EVENT) {
        shutdown_flag = true;
    }
    return TRUE; /* ignore */
}
#endif

int main(int argc, char **argv) {
    auto bootstrap_guard = utils::bootstrap_guard_ptr_t();

#if defined(__unix__) || defined(__APPLE__) || defined(__MACH__)
    struct sigaction act;
    memset(&act, 0, sizeof(act));
    act.sa_handler = [](int) { shutdown_flag = true; };
    if (sigaction(SIGINT, &act, nullptr) != 0) {
        spdlog::critical("cannot set signal handler");
        return 1;
    }
#endif
#ifdef _WIN32
    if (!SetConsoleCtrlHandler(consoleHandler, true)) {
        spdlog::critical("ERROR: Could not set control handler");
        return -1;
    }
#endif
    try {
#ifdef _WIN32
#endif
        utils::platform_t::startup();

#if defined(__linux__)
        pthread_setname_np(pthread_self(), "ss/main");
#endif
        auto dist_sink = utils::create_root_logger();
        dist_sink->add_sink(std::make_shared<spdlog::sinks::stdout_color_sink_mt>());

        // clang-format off
        /* parse command-line & config options */
        po::options_description cmdline_descr("Allowed options");
        using CommandStrings = std::vector<std::string>;
        cmdline_descr.add_options()
            ("help", "show this help message")
            ("log_level", po::value<std::string>()->default_value("info"),
                        "initial log level")
            ("config_dir", po::value<std::string>(),
                        "configuration directory path")
            ("command", po::value<CommandStrings>(), "command for a deamon. "
                "An arg can be:\n"
                "  add_peer:${label}:${device_id} - records peer in a database;"
                    " tries to connect to it and  allows incoming connections"
                    " from peer device;\n"
                "  add_folder:label=${folder_label}:id=${folder_id}:path=${path}"
                    " adds a folder into the database, scans folder there"
                    " (and adds files from it into the database). The"
                    " ${folder_label} is arbitrary human-readable local label,"
                    " while {folder_id} is predefined folder id to be shared"
                    " with other devices. The ${path} is local path to the"
                    " folder, e.g. /storage/music;\n"
                "  share:folder=${label}:device=${device} shares the folder"
                    " with the device. The ${label} can be a folder label or"
                    " folder id, and the device can be a device id, device"
                    " label or the first part of the device id, e.g. for device"
                    " id 'XBOWTOU-Y7H6RM6-D7WT3UB-7P2DZ5G-R6GNZG6-T5CCG54-SGVF3U5-LBM7RQB'"
                    " the first part is 'XBOWTOU';\n"
                "  inactivate:${seconds} shut down daemon after ${seconds}"
                    " of inactivity;\n"
            );
        // clang-format on

        po::variables_map vm;
        po::store(po::parse_command_line(argc, argv, cmdline_descr), vm);
        po::notify(vm);

        bool show_help = vm.count("help");
        if (show_help) {
            std::cout << cmdline_descr << "\n";
            return 1;
        }

        auto log_level = utils::get_log_level(vm["log_level"].as<std::string>());

        bfs::path config_file_path;
        if (vm.count("config_dir")) {
            auto path = vm["config_dir"].as<std::string>();
            config_file_path = bfs::path{path.c_str()};
        } else {
            auto config_default = utils::get_default_config_dir();
            if (config_default) {
                config_file_path = config_default.value();
            } else {
                spdlog::error("cannot determine default config dir: {}", config_default.error().message());
                return 1;
            }
        }
        bootstrap_guard = utils::bootstrap(dist_sink, config_file_path);

        config_file_path.append("syncspirit.toml");
        auto config_file_path_str = config_file_path.string();
        bool populate = !bfs::exists(config_file_path);
        if (populate) {
            spdlog::info("Config {} seems does not exit, creating default one...", config_file_path.string());
            auto cfg_opt = config::generate_config(config_file_path);
            if (!cfg_opt) {
                spdlog::error("cannot generate default config: {}", cfg_opt.error().message());
                return 1;
            }
            auto &cfg = cfg_opt.value();
            using F = utils::fstream_t;
            auto f_cfg = utils::fstream_t(config_file_path, F::binary | F::trunc | F::in | F::out);
            auto r = config::serialize(cfg, f_cfg);
            if (!r) {
                spdlog::error("cannot save default config at {}: {}", config_file_path_str, r.error().message());
                return 1;
            }
        }
        auto config_file = utils::ifstream_t(config_file_path);
        if (!config_file) {
            spdlog::error("Cannot open config file {}", config_file_path_str);
            return 1;
        }

        config::config_result_t cfg_option = config::get_config(config_file, config_file_path.parent_path());
        if (!cfg_option) {
            spdlog::error("Config file {} is incorrect :: {}", config_file_path_str, cfg_option.error());
            return 1;
        }
        auto &cfg = cfg_option.value();
        spdlog::trace("configuration seems OK");

        // override default
        if (log_level) {
            if (cfg.log_configs.size()) {
                auto &first_cfg = cfg.log_configs.front();
                if (first_cfg.name == "default") {
                    auto level = log_level.value();
                    first_cfg.level = level;
                    spdlog::trace("overriding default log level to {}", int(level));
                }
            }
        }
        auto init_result = utils::init_loggers(cfg.log_configs);
        if (!init_result) {
            spdlog::error("Loggers initialization failed :: {}", init_result.error().message());
            return 1;
        }

        Commands commands;
        if (vm.count("command")) {
            auto cmds = vm["command"].as<CommandStrings>();
            for (auto &cmd : cmds) {
                auto r = command_t::parse(cmd);
                if (!r) {
                    spdlog::error("error parsing {} : {}", cmd, r.assume_error().message());
                    return 1;
                }
                commands.emplace_back(std::move(r.assume_value()));
            }
        }

        if (populate) {
            spdlog::info("Generating cryptographic keys...");
            auto pair = utils::generate_pair(constants::issuer_name);
            if (!pair) {
                spdlog::error("cannot generate cryptographic keys :: {}", pair.error().message());
                return 1;
            }
            auto &keys = pair.value();
            auto &cert_path = cfg.global_announce_config.cert_file;
            auto &key_path = cfg.global_announce_config.key_file;
            auto save_result = keys.save(cert_path.c_str(), key_path.c_str());
            if (!save_result) {
                spdlog::error("cannot store cryptographic keys ({} & {}) :: {}", cert_path, key_path,
                              save_result.error().message());
                return 1;
            }
        }

        spdlog::info("starting {} {}, libraries: lz4: v{}, OpenSSL {}", constants::client_name, SYNCSPIRIT_VERSION,
                     LZ4_versionString(), OpenSSL_version(0));

        /* pre-init actors */
        asio::io_context io_context;
        ra::system_context_ptr_t sys_context{new asio_sys_context_t{io_context}};
        auto strand = std::make_shared<asio::io_context::strand>(io_context);
        auto timeout = pt::milliseconds{cfg.timeout};

        auto cluster_copies = 1ul;
        auto seed = (size_t)std::time(nullptr);
        auto sequencer = model::make_sequencer(seed);

        auto sup_net = sys_context->create_supervisor<net::net_supervisor_t>()
                           .app_config(cfg)
                           .strand(strand)
                           .timeout(timeout)
                           .create_registry()
                           .guard_context(true)
                           .sequencer(sequencer)
                           .cluster_copies(cluster_copies)
                           .shutdown_flag(shutdown_flag, r::pt::millisec{50})
                           .finish();
        sup_net->start();
        // pre-startup
        sup_net->do_process();

        thread_sys_context_t fs_context;
        auto fs_sup = fs_context.create_supervisor<syncspirit::fs::fs_supervisor_t>()
                          .timeout(timeout)
                          .registry_address(sup_net->get_registry_address())
                          .fs_config(cfg.fs_config)
                          .hasher_threads(cfg.hasher_threads)
                          .sequencer(sequencer)
                          .finish();

        // auxiliary payload
        fs_sup->add_launcher([&](model::cluster_ptr_t &cluster) mutable {
            fs_sup->create_actor<governor_actor_t>()
                .commands(std::move(commands))
                .cluster(cluster)
                .sequencer(sequencer)
                .timeout(timeout)
                .autoshutdown_supervisor()
                .finish();
        });

        auto hasher_count = cfg.hasher_threads;
        using sys_thread_context_ptr_t = r::intrusive_ptr_t<thread_sys_context_t>;
        std::vector<sys_thread_context_ptr_t> hasher_ctxs;
        for (uint32_t i = 1; i <= hasher_count; ++i) {
            hasher_ctxs.push_back(new thread_sys_context_t{});
            auto &ctx = hasher_ctxs.back();
            ctx->create_supervisor<hasher::hasher_supervisor_t>()
                .timeout(timeout / 2)
                .registry_address(sup_net->get_registry_address())
                .index(i)
                .finish();
        }

        /* launch actors */
        auto fs_thread = std::thread([&]() {
#if defined(__linux__)
            pthread_setname_np(pthread_self(), "ss/fs");
#endif
            fs_context.run();
            shutdown_flag = true;
            spdlog::trace("fs thread has been terminated");
        });

        auto hasher_threads = std::vector<std::thread>();
        for (uint32_t i = 0; i < hasher_count; ++i) {
            auto &ctx = hasher_ctxs.at(i);
            auto thread = std::thread([ctx = ctx, i = i]() {
#if defined(__linux__)
                std::string name = "ss/hasher-" + std::to_string(i + 1);
                pthread_setname_np(pthread_self(), name.c_str());
#endif
                ctx->run();
                shutdown_flag = true;
#if defined(__linux__)
                spdlog::trace("{} thread has been terminated", name);
#endif
            });
            hasher_threads.emplace_back(std::move(thread));
        }

        // main loop;
#if defined(__linux__)
        pthread_setname_np(pthread_self(), "ss/net");
#endif
        io_context.run();
        shutdown_flag = true;
        spdlog::trace("net thread has been terminated");

        spdlog::trace("waiting fs thread termination");
        fs_thread.join();

        spdlog::trace("waiting hasher threads termination");
        for (auto &thread : hasher_threads) {
            thread.join();
        }
        spdlog::trace("everything has been terminated");
    } catch (const po::error &ex) {
        spdlog::critical("program options exception: {}", ex.what());
        return 1;
    } catch (std::exception &ex) {
        spdlog::critical("std::exception: {}", ex.what());
        return 1;
    } catch (...) {
        spdlog::critical("unknown exception");
        return 1;
    }

    utils::platform_t::shutdown();
    /* exit */

    spdlog::info("normal exit");
    bootstrap_guard.reset();
    spdlog::drop_all();
    return 0;
}


code:
cmake_minimum_required (VERSION 3.19)

find_package(libqrencode REQUIRED)

if(MINGW)
    enable_language(RC)
endif()

add_executable(syncspirit-fltk WIN32 MACOSX_BUNDLE
    app_supervisor.cpp
    augmentation.cpp
    content.cpp
    log_colors.cpp
    log_panel.cpp
    log_sink.cpp
    log_table.cpp
    log_utils.cpp
    main.cpp
    main_window.cpp
    presence_item.cpp
    qr_button.cpp
    static_table.cpp
    symbols.cpp
    toolbar.cpp
    tree_item.cpp
    tree_view.cpp
    utils.cpp
    config/category.cpp
    config/control.cpp
    config/properties.cpp
    config/property.cpp
    config/table.cpp
    content/file_table.cpp
    content/folder_table.cpp
    presence_item/cluster.cpp
    presence_item/file.cpp
    presence_item/folder.cpp
    presence_item/missing.cpp
    table_widget/checkbox.cpp
    table_widget/choice.cpp
    table_widget/input.cpp
    table_widget/int_input.cpp
    table_widget/label.cpp
    table_widget/path.cpp
    tree_item/devices.cpp
    tree_item/folders.cpp
    tree_item/ignored_device.cpp
    tree_item/ignored_devices.cpp
    tree_item/peer_device.cpp
    tree_item/peer_folders.cpp
    tree_item/pending_device.cpp
    tree_item/pending_devices.cpp
    tree_item/pending_folder.cpp
    tree_item/pending_folders.cpp
    tree_item/self_device.cpp
    tree_item/settings.cpp
    $<$<PLATFORM_ID:Windows>:app.rc>
    $<$<PLATFORM_ID:Darwin>:${syncspirit_SOURCE_DIR}/misc/SyncSpiritIcon.icns>
)

target_link_libraries(syncspirit-fltk
    syncspirit_core
    rotor::fltk
    $<$<PLATFORM_ID:Linux>:jpeg>
    $<$<PLATFORM_ID:Linux>:png>
    libqrencode::libqrencode
)
target_include_directories(syncspirit-fltk PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}/
    ${CMAKE_CURRENT_BINARY_DIR}/fltk
    ${syncspirit_SOURCE_DIR}/misc
    ${syncspirit_SOURCE_DIR}/src
    ${syncspirit_SOURCE_DIR}/lib/fltk
)

install_executable("syncspirit-fltk")
#install(TARGETS "syncspirit-fltk")

if (("${CMAKE_BUILD_TYPE}" STREQUAL "Release") AND (NOT WIN32))
    include(CheckIPOSupported)
    check_ipo_supported(RESULT supported OUTPUT error)
    if( supported )
        message(STATUS "IPO / LTO enabled for the syncspirit-fltk")
        set_property(TARGET syncspirit-fltk PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
    else()
        message(STATUS "IPO / LTO not supported")
    endif()
endif()

if (CMAKE_BUILD_TYPE MATCHES "^([Rr]elease)|(MinSizeRel)")
    set_target_properties(syncspirit-fltk PROPERTIES LINK_FLAGS -s)
endif()

set_target_properties(syncspirit-fltk PROPERTIES
    MACOSX_BUNDLE_BUNDLE_NAME    syncspirit
    MACOSX_BUNDLE_GUI_IDENTIFIER "com.example.syncspirit-fltk"
    MACOSX_BUNDLE_BUNDLE_VERSION "${CMAKE_PROJECT_VERSION}"
    MACOSX_BUNDLE_SHORT_VERSION_STRING "${CMAKE_PROJECT_VERSION}"
    MACOSX_BUNDLE_ICON_FILE "SyncSpiritIcon.icns"
    XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY TRUE
)
set_source_files_properties(${syncspirit_SOURCE_DIR}/misc/SyncSpiritIcon.icns PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")

repo_name: basiliscos/syncspirit

license: mit


code:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
  <assemblyIdentity type="win32" name="Organization.Division.Name" version="1.0.0.0"/>
  <asmv3:application>
     <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">
       <activeCodePage>UTF-8</activeCodePage>
     </asmv3:windowsSettings>
   </asmv3:application>
</assembly>


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2025 Ivan Baidakou

#pragma once

#define ID_SYNCSPIRIT_ICON 1000


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2025 Ivan Baidakou

#include <windows.h>    // RT_MANIFEST
#include "app.h"
// CREATEPROCESS_MANIFEST_RESOURCE_ID is defined as 1 cast to `char*`; not directly usable here.

1                    RT_MANIFEST    "app-manifest.xml"
ID_SYNCSPIRIT_ICON   ICON           "syncspirit-fltk.ico"


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024-2025 Ivan Baidakou

#include "app_supervisor.h"
#include "augmentation.h"
#include "main_window.h"
#include "presence_item/folder.h"
#include "tree_item/devices.h"
#include "tree_item/folders.h"
#include "tree_item/ignored_devices.h"
#include "tree_item/peer_device.h"
#include "tree_item/pending_devices.h"
#include "tree_item/pending_folders.h"
#include "tree_item/peer_folders.h"
#include "net/names.h"
#include "config/utils.h"
#include "model/diff/advance/advance.h"
#include "model/diff/local/io_failure.h"
#include "model/diff/local/scan_start.h"
#include "model/diff/load/blocks.h"
#include "model/diff/load/file_infos.h"
#include "model/diff/load/load_cluster.h"
#include "model/diff/modify/add_ignored_device.h"
#include "model/diff/modify/add_pending_device.h"
#include "model/diff/modify/add_pending_folders.h"
#include "model/diff/modify/upsert_folder.h"
#include "model/diff/modify/upsert_folder_info.h"
#include "model/diff/modify/update_peer.h"
#include "model/diff/modify/share_folder.h"
#include "model/diff/peer/update_folder.h"
#include "presentation/entity.h"
#include "presentation/folder_entity.h"
#include "presentation/folder_presence.h"
#include "utils/format.hpp"
#include "utils/io.h"

#include <utility>
#include <sstream>
#include <iomanip>
#include <functional>
#include <limits>
#include <memory_resource>
#include <unordered_set>

using namespace syncspirit::fltk;
using namespace syncspirit::presentation;

static auto MAX_DEPTH = std::numeric_limits<std::int32_t>::max();

namespace {
namespace resource {
r::plugin::resource_id_t model = 0;
}
} // namespace

using entities_ptrs_t = std::pmr::unordered_set<const entity_t *>;
using entities_t = std::pmr::unordered_set<entity_ptr_t>;
using guards_t = std::pmr::vector<entity_t::monitor_guard_t>;
using shared_device_t = std::pmr::vector<syncspirit::model::device_t *>;

struct app_monitor_t final : entities_monitor_t {
    app_monitor_t(entities_t &deleted_, entities_ptrs_t &updated_) : deleted{deleted_}, updated{updated_} {}
    void on_delete(entity_t &entity) noexcept override {
        deleted.emplace(entity_ptr_t{&entity});
        if (auto parent = entity.get_parent(); parent) {
            on_update(*parent);
        }
    }
    void on_update(const entity_t &entity) noexcept override {
        auto current = &entity;
        while (current) {
            updated.insert(current);
            current = current->get_parent();
        }
    }

    entities_t &deleted;
    entities_ptrs_t &updated;
};

struct visitor_context_t {
    guards_t &guards;
    app_monitor_t &monitor;
};

db_info_viewer_guard_t::db_info_viewer_guard_t(main_window_t *main_window_) : main_window{main_window_} {}
db_info_viewer_guard_t::db_info_viewer_guard_t(db_info_viewer_guard_t &&other) { *this = std::move(other); }

db_info_viewer_guard_t &db_info_viewer_guard_t::operator=(db_info_viewer_guard_t &&other) {
    std::swap(main_window, other.main_window);
    return *this;
}

db_info_viewer_guard_t::~db_info_viewer_guard_t() {
    if (main_window) {
        if (auto sup = main_window->get_supervisor(); sup) {
            sup->log->trace("~db_info_viewer_guard_t");
            sup->db_info_viewer = nullptr;
        }
    }
}

void db_info_viewer_guard_t::reset() {
    if (main_window) {
        if (auto sup = main_window->get_supervisor(); sup) {
            sup->db_info_viewer = nullptr;
        }
        main_window = nullptr;
    }
}

using callback_fn_t = std::function<void()>;

struct callback_impl_t final : callback_t {
    callback_impl_t(callback_fn_t fn_) : fn{std::move(fn_)} {}

    void eval() override { fn(); }

    callback_fn_t fn;
};

static void ui_idle_means_ready(void *data) {
    auto sup = reinterpret_cast<app_supervisor_t *>(data);
    sup->send<syncspirit::model::payload::ui_ready_t>(sup->get_coordinator_address());
    Fl::remove_idle(ui_idle_means_ready, data);
}

app_supervisor_t::app_supervisor_t(config_t &config)
    : parent_t(config), dist_sink(std::move(config.dist_sink)), config_path{std::move(config.config_path)},
      app_config(std::move(config.app_config)), app_config_original{app_config}, content{nullptr}, devices{nullptr},
      folders{nullptr}, pending_devices{nullptr}, ignored_devices{nullptr}, db_info_viewer{nullptr},
      main_window{nullptr}, loaded_blocks{0}, loaded_files{0} {
    started_at = clock_t::now();
    sequencer = model::make_sequencer(started_at.time_since_epoch().count());
}

app_supervisor_t::~app_supervisor_t() {
    cluster.reset();
    if (ui_sink) {
        dist_sink->remove_sink(ui_sink);
        ui_sink.reset();
    }
    if (main_window) {
        main_window->detach_supervisor();
    }
    spdlog::debug("~app_supervisor_t()");
}

auto app_supervisor_t::get_dist_sink() -> dist_sink_t & { return dist_sink; }
auto app_supervisor_t::get_config_path() -> const bfs::path & { return config_path; }
auto app_supervisor_t::get_app_config() -> config::main_t & { return app_config; }
auto app_supervisor_t::get_cluster() -> model::cluster_t * { return cluster.get(); }
auto app_supervisor_t::get_sequencer() -> model::sequencer_t & { return *sequencer; }

void app_supervisor_t::configure(r::plugin::plugin_base_t &plugin) noexcept {
    parent_t::configure(plugin);
    plugin.with_casted<r::plugin::address_maker_plugin_t>([&](auto &p) {
        p.set_identity("fltk", false);
        log = utils::get_logger(identity);
    });
    plugin.with_casted<r::plugin::registry_plugin_t>([&](auto &p) {
        p.discover_name(net::names::coordinator, coordinator, true).link(false).callback([&](auto phase, auto &ee) {
            if (!ee && phase == r::plugin::registry_plugin_t::phase_t::linking) {
                auto p = get_plugin(r::plugin::starter_plugin_t::class_identity);
                auto plugin = static_cast<r::plugin::starter_plugin_t *>(p);
                plugin->subscribe_actor(&app_supervisor_t::on_model_update, coordinator);
                request_load_model();
            }
        });
        p.discover_name(net::names::sink, sink, false).link(true);
    });

    plugin.with_casted<r::plugin::starter_plugin_t>(
        [&](auto &p) {
            p.subscribe_actor(&app_supervisor_t::on_model_response);
            p.subscribe_actor(&app_supervisor_t::on_db_info_response);
        },
        r::plugin::config_phase_t::PREINIT);
}

void app_supervisor_t::shutdown_finish() noexcept {
    parent_t::shutdown_finish();
    LOG_TRACE(log, "shutdown_finish");
    if (main_window) {
        main_window->on_shutdown();
        main_window->detach_supervisor();
        main_window = nullptr;
    }
    std::stringstream out;
    std::stringstream out_orig;
    auto r = config::serialize(app_config, out);
    auto r_orig = config::serialize(app_config_original, out_orig);
    if (r.has_value() && r_orig.has_value()) {
        if (out.str() != out_orig.str()) {
            write_config(app_config);
        }
    }
}

void app_supervisor_t::request_load_model() {
    request<model::payload::model_request_t>(coordinator).send(init_timeout);
    resources->acquire(resource::model);
}

auto app_supervisor_t::get_coordinator_address() -> r::address_ptr_t & { return coordinator; }

void app_supervisor_t::on_model_response(model::message::model_response_t &res) noexcept {
    LOG_TRACE(log, "on_model_response");
    resources->release(resource::model);
    auto &ee = res.payload.ee;
    if (ee) {
        LOG_ERROR(log, "cannot get model: {}", ee->message());
        return do_shutdown(ee);
    }
    cluster = std::move(res.payload.res.cluster);
}

void app_supervisor_t::on_model_update(model::message::model_update_t &message) noexcept {
    LOG_TRACE(log, "on_model_update");
    bool has_been_loaded = cluster->get_devices().size();

    auto buffer = std::array<std::byte, 16 * 1024>();
    auto pool = std::pmr::monotonic_buffer_resource(buffer.data(), buffer.size());
    auto allocator = std::pmr::polymorphic_allocator<std::string>(&pool);

    auto updated_entities = entities_ptrs_t(allocator);
    auto deleted_entities = entities_t(allocator);
    auto monitor = app_monitor_t(deleted_entities, updated_entities);

    auto &diff = *message.payload.diff;
    auto &folders = cluster->get_folders();
    auto guards = guards_t();
    guards.reserve(folders.size());
    for (auto &it : folders) {
        auto augmentation = (*it.item).get_augmentation().get();
        if (augmentation) {
            auto folder_entity = static_cast<presentation::folder_entity_t *>(augmentation);
            guards.emplace_back(folder_entity->monitor(&monitor));
        }
    }

    if (!has_been_loaded) {
        main_window->set_splash_text("populating model (1/3)...");
    }
    auto r = diff.apply(*cluster, *this);
    if (!r) {
        LOG_ERROR(log, "error applying cluster diff: {}", r.assume_error().message());
        return;
    }

    if (!has_been_loaded) {
        main_window->set_splash_text("populating model (2/3)...");
    }

    auto context = visitor_context_t{
        guards,
        monitor,
    };
    r = diff.visit(*this, &context);
    if (!r) {
        LOG_ERROR(log, "error visiting cluster diff: {}", r.assume_error().message());
        return;
    }

    auto custom = message.payload.custom;
    if (custom) {
        for (auto it = begin(callbacks); it != end(callbacks); ++it) {
            if (it->get() == custom) {
                auto cb = *it;
                callbacks.erase(it);
                cb->eval();
                break;
            }
        }
    }

    if (!has_been_loaded) {
        main_window->set_splash_text("populating model (3/3)...");
    }

    for (auto &entity : deleted_entities) {
        updated_entities.erase(entity.get());
    }
    for (auto entity : updated_entities) {
        for (auto p : entity->get_presences()) {
            auto augmentation = p->get_augmentation().get();
            if (augmentation) {
                auto item = static_cast<presence_item_t *>(augmentation);
                item->on_update();
            }
        }
    }

    if (!has_been_loaded && cluster->get_devices().size()) {
        main_window->on_loading_done();
        main_window->set_splash_text("UI has been initialized");
        Fl::add_idle(ui_idle_means_ready, this);
    }
}

std::string app_supervisor_t::get_uptime() noexcept {
    using namespace std;
    using namespace std::chrono;
    auto uptime = clock_t::now() - started_at;
    auto h = duration_cast<hours>(uptime);
    uptime -= h;
    auto m = duration_cast<minutes>(uptime);
    uptime -= m;
    auto s = duration_cast<seconds>(uptime);
    uptime -= s;

    std::stringstream out;
    out.fill('0');
    out << setw(2) << h.count() << ":" << setw(2) << m.count() << ":" << setw(2) << s.count();

    return out.str();
}

void app_supervisor_t::on_db_info_response(net::message::db_info_response_t &res) noexcept {
    if (db_info_viewer) {
        auto &ee = res.payload.ee;
        if (ee) {
            log->warn("error requesting db info: {}", ee->message());
        } else {
            db_info_viewer->view(res.payload.res);
        }
    }
}

auto app_supervisor_t::get_logger() noexcept -> utils::logger_t & { return log; }
void app_supervisor_t::set_devices(tree_item_t *node) { devices = node; }
void app_supervisor_t::set_folders(tree_item_t *node) { folders = node; }
void app_supervisor_t::set_pending_devices(tree_item_t *node) { pending_devices = node; }
void app_supervisor_t::set_ignored_devices(tree_item_t *node) { ignored_devices = node; }
void app_supervisor_t::set_main_window(main_window_t *window) { main_window = window; }
main_window_t *app_supervisor_t::get_main_window() { return main_window; }

void app_supervisor_t::add_sink(spdlog::sink_ptr ui_sink_) {
    ui_sink = ui_sink_;
    dist_sink->add_sink(ui_sink);
}

auto app_supervisor_t::request_db_info(db_info_viewer_t *viewer) -> db_info_viewer_guard_t {
    log->trace("request_db_info");
    request<net::payload::db_info_request_t>(coordinator).send(init_timeout * 5 / 6);
    db_info_viewer = viewer;
    return db_info_viewer_guard_t(main_window);
}

callback_ptr_t app_supervisor_t::call_select_folder(std::string_view folder_id) {
    auto id = std::string(folder_id);
    auto fn = callback_fn_t([this, id = std::move(id)]() {
        auto folders_node = static_cast<tree_item::folders_t *>(folders);
        folders_node->select_folder(id);
    });
    auto cb = callback_ptr_t(new callback_impl_t(std::move(fn)));
    callbacks.push_back(cb);
    return cb;
}

callback_ptr_t app_supervisor_t::call_share_folders(std::string_view folder_id, std::vector<utils::bytes_t> devices) {
    assert(devices.size());
    auto fn = callback_fn_t([this, folder_id = std::string(folder_id), devices = std::move(devices)]() {
        auto diff = model::diff::cluster_diff_ptr_t{};
        auto current = diff.get();
        auto &self = cluster->get_device()->device_id();
        for (auto &sha256 : devices) {
            auto device = cluster->get_devices().by_sha256(sha256);
            if (!device) {
                log->error("cannot share folder {}: target device is missing", folder_id);
                return;
            }
            auto folder = cluster->get_folders().by_id(folder_id);
            if (!folder) {
                log->error("cannot share folder {}: not such a folder", folder_id);
                return;
            }
            using diff_t = model::diff::modify::share_folder_t;
            auto opt = diff_t::create(*cluster, *sequencer, *device, self, *folder);
            if (!opt) {
                auto message = opt.assume_error().message();
                log->error("cannot share folder {} with {} : {}", folder_id, device->device_id(), message);
                return;
            }
            auto &sub_diff = opt.assume_value();
            if (!current) {
                diff = sub_diff;
                current = diff.get();
            } else {
                current = current->assign_sibling(sub_diff.get());
            }
        }
        auto cb = call_select_folder(folder_id);
        send_model<model::payload::model_update_t>(std::move(diff), cb.get());
    });
    auto cb = callback_ptr_t(new callback_impl_t(std::move(fn)));
    callbacks.push_back(cb);
    return cb;
}

auto app_supervisor_t::operator()(const model::diff::load::load_cluster_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    if (!devices) {
        return diff.visit_next(*this, custom);
    }
    auto folders_node = static_cast<tree_item::folders_t *>(folders);
    for (auto &it : cluster->get_folders()) {
        auto &folder = it.item;
        auto text = fmt::format("building model of cluster folder '{}'({}) ...", folder->get_label(), folder->get_id());
        main_window->set_splash_text(text);
        auto folder_entity = folder_entity_ptr_t(new folder_entity_t(folder));
        folders_node->add_folder(*folder_entity);
        folder->set_augmentation(folder_entity);
    }

    auto devices_node = static_cast<tree_item::devices_t *>(devices);

    auto &self_device = cluster->get_device();
    auto self_node = devices_node->set_self(*cluster->get_device());
    auto tree = self_node->get_owner()->tree();
    tree->select(self_node->get_owner());
    self_device->set_augmentation(*self_node);

    for (auto &it : cluster->get_devices()) {
        auto &device = *it.item;
        if (device.device_id() != cluster->get_device()->device_id()) {
            auto text = fmt::format("populating device '{}'({})...", device.get_name(), device.device_id().get_short());
            main_window->set_splash_text(text);
            device.set_augmentation(devices_node->add_peer(device));
        }
    }

    auto pending_devices_node = static_cast<tree_item::pending_devices_t *>(pending_devices);
    for (auto &it : cluster->get_pending_devices()) {
        auto &device = *it.item;
        device.set_augmentation(pending_devices_node->add_device(device));
    }

    auto ignored_devices_node = static_cast<tree_item::ignored_devices_t *>(ignored_devices);
    for (auto &it : cluster->get_ignored_devices()) {
        auto &device = *it.item;
        device.set_augmentation(ignored_devices_node->add_device(device));
    }

    return diff.visit_next(*this, custom);
}

auto app_supervisor_t::operator()(const model::diff::local::io_failure_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    for (auto &details : diff.errors) {
        log->warn("I/O error on '{}': {}", details.path.string(), details.ec.message());
    }
    return diff.visit_next(*this, custom);
}

auto app_supervisor_t::operator()(const model::diff::modify::update_peer_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    auto device = cluster->get_devices().by_sha256(diff.peer_id);
    auto augmentation = device->get_augmentation();
    if (!augmentation) {
        auto devices_node = static_cast<tree_item::devices_t *>(devices);
        device->set_augmentation(devices_node->add_peer(*device));
    }
    return diff.visit_next(*this, custom);
}

auto app_supervisor_t::operator()(const model::diff::modify::add_pending_folders_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    auto &devices = cluster->get_devices();
    auto &pending_folders = cluster->get_pending_folders();
    for (auto &item : diff.container) {
        auto peer = devices.by_sha256(item.peer_id);
        auto augmentation = static_cast<augmentation_t *>(peer->get_augmentation().get());
        auto peer_node = static_cast<tree_item::peer_device_t *>(augmentation->get_owner());
        auto pending_node = static_cast<tree_item::pending_folders_t *>(peer_node->get_pending_folders());
        auto folder_id = db::get_id(db::get_folder(item.db));
        auto pending_folder = pending_folders.by_id(folder_id);
        if (pending_folder->get_augmentation()) {
            continue;
        }
        pending_folder->set_augmentation(pending_node->add_pending_folder(*pending_folder));
    }
    return diff.visit_next(*this, custom);
}

auto app_supervisor_t::operator()(const model::diff::modify::add_pending_device_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    auto &device = *cluster->get_pending_devices().by_sha256(diff.device_id.get_sha256());
    auto pending_devices_node = static_cast<tree_item::pending_devices_t *>(pending_devices);
    device.set_augmentation(pending_devices_node->add_device(device));
    return diff.visit_next(*this, custom);
}

auto app_supervisor_t::operator()(const model::diff::modify::add_ignored_device_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    auto &device = *cluster->get_ignored_devices().by_sha256(diff.device_id.get_sha256());
    auto ignored_devices_node = static_cast<tree_item::ignored_devices_t *>(ignored_devices);
    device.set_augmentation(ignored_devices_node->add_device(device));
    return diff.visit_next(*this, custom);
}

auto app_supervisor_t::operator()(const model::diff::advance::advance_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    auto folder = cluster->get_folders().by_id(diff.folder_id);
    auto augmentation = folder->get_augmentation().get();
    auto folder_entity = static_cast<presentation::folder_entity_t *>(augmentation);
    if (folder_entity) {
        auto &folder_infos = folder->get_folder_infos();
        auto local_fi = folder_infos.by_device(*cluster->get_device());
        auto file_name = proto::get_name(diff.proto_local);
        auto local_file = local_fi->get_file_infos().by_name(file_name);
        if (local_file) {
            auto entity = folder_entity->on_insert(*local_file);
            if (entity) {
                auto parent = entity->get_parent();
                auto mask = mask_nodes();
                for (auto presence : entity->get_presences()) {
                    using F = presence_t::features_t;
                    if (!(presence->get_features() & F::missing)) {
                        auto parent_presence = presence->get_parent();
                        auto aug = parent_presence->get_augmentation().get();
                        if (aug) {
                            auto parent_item = static_cast<presence_item_t *>(aug);
                            parent_item->show_child(*presence, mask);
                            parent_item->tree()->redraw();
                        }
                    }
                }
            }
        }
    }
    return diff.visit_next(*this, custom);
}

auto app_supervisor_t::operator()(const model::diff::modify::upsert_folder_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    auto folder_id = db::get_id(diff.db);
    auto folder = cluster->get_folders().by_id(folder_id);
    if (!folder->get_augmentation()) {
        auto folders_node = static_cast<tree_item::folders_t *>(folders);
        auto folder_entity = folder_entity_ptr_t(new folder_entity_t(folder));
        folders_node->add_folder(*folder_entity);
        folder->set_augmentation(folder_entity);
        auto ctx = static_cast<visitor_context_t *>(custom);
        ctx->guards.emplace_back(folder_entity->monitor(&ctx->monitor));
    }
    return diff.visit_next(*this, custom);
}

auto app_supervisor_t::operator()(const model::diff::modify::upsert_folder_info_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    auto r = diff.visit_next(*this, custom);
    auto &folder = *cluster->get_folders().by_id(diff.folder_id);
    auto &device = *cluster->get_devices().by_sha256(diff.device_id);
    auto folder_info = folder.is_shared_with(device);
    if (&device != cluster->get_device()) {
        auto augmentation = folder.get_augmentation().get();
        auto folder_entity = static_cast<presentation::folder_entity_t *>(augmentation);
        auto folder_presence = folder_entity->on_insert(*folder_info);
        auto devices_node = static_cast<tree_item::devices_t *>(devices);
        auto peer_node = devices_node->get_peer(device);
        auto folders_node = static_cast<tree_item::peer_folders_t *>(peer_node->get_folders());
        if (!folder_presence->get_augmentation()) {
            folders_node->add_folder(*folder_presence);
        }
    }
    return r;
}

auto app_supervisor_t::operator()(const model::diff::peer::update_folder_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    auto buffer = std::array<std::byte, 128>();
    auto pool = std::pmr::monotonic_buffer_resource(buffer.data(), buffer.size());
    auto allocator = std::pmr::polymorphic_allocator<std::string>(&pool);
    auto shared_devices = shared_device_t(allocator);

    auto folder = cluster->get_folders().by_id(diff.folder_id);
    auto &devices_map = cluster->get_devices();

    for (auto &it : devices_map) {
        auto &device = it.item;
        if (folder->is_shared_with(*device)) {
            shared_devices.emplace_back(device.get());
        }
    }

    auto peer = devices_map.by_sha256(diff.peer_id);
    auto &files_map = folder->get_folder_infos().by_device(*peer)->get_file_infos();
    auto folder_aug = folder->get_augmentation().get();
    auto folder_entity = static_cast<presentation::folder_entity_t *>(folder_aug);
    auto mask = mask_nodes();
    for (auto &file : diff.files) {
        auto file_name = proto::get_name(file);
        auto file_info = files_map.by_name(file_name);
        auto augmentation = file_info->get_augmentation().get();
        if (!augmentation) {
            auto entity = folder_entity->on_insert(*file_info);
            if (entity) {
                auto parent_entity = entity->get_parent();
                if (parent_entity) {
                    for (auto device : shared_devices) {
                        auto presence = entity->get_presence(device);
                        auto parent = presence->get_parent();
                        if (!parent) {
                            parent = parent_entity->get_presence(device);
                        }
                        if (parent) {
                            auto parent_aug = parent->get_augmentation().get();
                            if (parent_aug) {
                                auto parent_item = static_cast<presence_item_t *>(parent_aug);
                                parent_item->show_child(*presence, mask);
                            }
                        }
                    }
                }
            }
        }
    }
    return diff.visit_next(*this, custom);
}

auto app_supervisor_t::apply(const model::diff::load::blocks_t &diff, model::cluster_t &cluster) noexcept
    -> outcome::result<void> {
    loaded_blocks += diff.blocks.size();
    auto share = (100. * loaded_blocks) / load_cluster->blocks_count;
    auto msg = fmt::format("({}%) loaded {} of {} blocks", (int)share, loaded_blocks, load_cluster->blocks_count);
    log->debug(msg);
    main_window->set_splash_text(msg);
    auto r = apply_controller_t::apply(diff, cluster);
    return r;
}

auto app_supervisor_t::apply(const model::diff::load::file_infos_t &diff, model::cluster_t &cluster) noexcept
    -> outcome::result<void> {
    loaded_files += diff.container.size();
    auto share = (100. * loaded_files) / load_cluster->files_count;
    auto msg = fmt::format("({}%) loaded {} of {} files", (int)share, loaded_files, load_cluster->files_count);
    log->debug(msg);
    main_window->set_splash_text(msg);
    auto r = apply_controller_t::apply(diff, cluster);
    return r;
}

auto app_supervisor_t::apply(const model::diff::load::load_cluster_t &diff, model::cluster_t &cluster) noexcept
    -> outcome::result<void> {
    load_cluster = &diff;
    auto r = apply_controller_t::apply(diff, cluster);
    load_cluster = nullptr;
    return r;
}

void app_supervisor_t::write_config(const config::main_t &cfg) noexcept {
    using F = utils::fstream_t;
    log->debug("going to write config");
    auto &path = get_config_path();
    utils::fstream_t f_cfg(path.string(), F::binary | F::trunc | F::in | F::out);
    auto r = config::serialize(cfg, f_cfg);
    if (!r) {
        log->error("cannot save default config at {}: {}", path, r.error().message());
    } else {
        log->info("succesfully stored config at {}. Restart to apply", path);
    }
    app_config_original = app_config = cfg;
}

void app_supervisor_t::redisplay_folder_nodes(bool refresh_labels) {
    auto mask = mask_nodes();
    log->debug("redisplay_folder_nodes, mask: {:#x}", mask);
    for (auto &it_f : cluster->get_folders()) {
        for (auto &it : it_f.item->get_folder_infos()) {
            auto generic_augmentation = it.item->get_augmentation();
            if (generic_augmentation) {
                auto presence = static_cast<presentation::presence_t *>(generic_augmentation.get());
                auto item = dynamic_cast<presence_item_t *>(presence->get_augmentation().get());
                if (item) {
                    item->show(mask, refresh_labels, MAX_DEPTH);
                }
            }
        }
    }
}

void app_supervisor_t::set_show_deleted(bool value) {
    app_config.fltk_config.display_deleted = value;
    redisplay_folder_nodes(false);
}

void app_supervisor_t::set_show_missing(bool value) {
    app_config.fltk_config.display_missing = value;
    redisplay_folder_nodes(false);
}

void app_supervisor_t::set_show_colorized(bool value) {
    log->debug("display colorized = {}", value);
    app_config.fltk_config.display_colorized = value;
    redisplay_folder_nodes(true);
}

std::uint32_t app_supervisor_t::mask_nodes() const noexcept {
    using F = syncspirit::presentation::presence_t::features_t;
    auto r = std::uint32_t{0};
    if (!app_config.fltk_config.display_deleted) {
        r |= F::deleted;
    }
    if (!app_config.fltk_config.display_missing) {
        r |= F::missing;
    }
    return r;
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024-2025 Ivan Baidakou

#pragma once

#include "content.h"
#include "config/main.h"
#include "utils/log.h"
#include "net/messages.h"
#include "model/messages.h"
#include "model/diff/apply_controller.h"
#include "model/diff/cluster_visitor.h"
#include "model/diff/load/load_cluster.h"
#include "model/misc/sequencer.h"

#include <spdlog/sinks/dist_sink.h>
#include <rotor/fltk.hpp>
#include <FL/Fl_Widget.H>
#include <FL/Fl_Group.H>
#include <filesystem>
#include <chrono>
#include <set>

namespace syncspirit::fltk {

namespace r = rotor;
namespace rf = r::fltk;
namespace bfs = std::filesystem;
namespace sys = boost::system;
namespace outcome = boost::outcome_v2;

struct app_supervisor_t;
struct main_window_t;
struct tree_item_t;
struct augmentation_entry_base_t;

struct db_info_viewer_t {
    virtual void view(const net::payload::db_info_response_t &) = 0;
};

struct db_info_viewer_guard_t {
    db_info_viewer_guard_t(main_window_t *main_window);
    db_info_viewer_guard_t(const db_info_viewer_guard_t &) = delete;
    db_info_viewer_guard_t(db_info_viewer_guard_t &&);

    db_info_viewer_guard_t &operator=(db_info_viewer_guard_t &&);
    ~db_info_viewer_guard_t();
    void reset();

  private:
    main_window_t *main_window;
};

using dist_sink_t = std::shared_ptr<spdlog::sinks::dist_sink_mt>;

struct app_supervisor_config_t : rf::supervisor_config_fltk_t {
    using parent_t = rf::supervisor_config_fltk_t;
    using parent_t::parent_t;

    dist_sink_t dist_sink;
    bfs::path config_path;
    config::main_t app_config;
};

template <typename Actor> struct app_supervisor_config_builder_t : rf::supervisor_config_fltk_builder_t<Actor> {
    using builder_t = typename Actor::template config_builder_t<Actor>;
    using parent_t = rf::supervisor_config_fltk_builder_t<Actor>;
    using parent_t::parent_t;

    builder_t &&dist_sink(dist_sink_t value) && noexcept {
        parent_t::config.dist_sink = value;
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }

    builder_t &&config_path(const bfs::path &value) && noexcept {
        parent_t::config.config_path = value;
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }

    builder_t &&app_config(const config::main_t &value) && noexcept {
        parent_t::config.app_config = value;
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }
};

struct callback_t : model::arc_base_t<callback_t> {
    virtual ~callback_t() = default;
    virtual void eval() = 0;
};
using callback_ptr_t = model::intrusive_ptr_t<callback_t>;

struct app_supervisor_t : rf::supervisor_fltk_t,
                          private model::diff::cluster_visitor_t,
                          private model::diff::apply_controller_t {
    using parent_t = rf::supervisor_fltk_t;
    using config_t = app_supervisor_config_t;
    template <typename Actor> using config_builder_t = app_supervisor_config_builder_t<Actor>;

    explicit app_supervisor_t(config_t &config);
    app_supervisor_t(const app_supervisor_t &) = delete;
    ~app_supervisor_t();

    void configure(r::plugin::plugin_base_t &plugin) noexcept override;
    void shutdown_finish() noexcept override;

    dist_sink_t &get_dist_sink();
    const bfs::path &get_config_path();
    config::main_t &get_app_config();
    model::cluster_t *get_cluster();
    model::sequencer_t &get_sequencer();
    void write_config(const config::main_t &) noexcept;

    std::string get_uptime() noexcept;
    utils::logger_t &get_logger() noexcept;
    void add_sink(spdlog::sink_ptr ui_sink);

    template <typename Fn> auto replace_content(Fn constructor) noexcept -> content_t * {
        if (!content) {
            content = constructor(content);
        } else {
            auto parent = content->get_widget()->parent();
            auto prev = content;
            parent->remove(prev->get_widget());
            parent->begin();
            content = constructor(prev);
            log->trace("replacing content {} -> {}", (void *)prev, (void *)content);
            content->refresh();
            parent->add(content->get_widget());
            parent->end();
            delete prev;
            // parent->resizable(content);
            parent->redraw();
        }
        return content;
    }

    template <typename Payload, typename... Args> void send_model(Args &&...args) {
        send<Payload>(coordinator, std::forward<Args>(args)...);
    }

    template <typename Payload, typename... Args> void send_sink(Args &&...args) {
        auto message = r::make_routed_message<Payload>(coordinator, sink, std::forward<Args>(args)...);
        put(std::move(message));
    }

    template <typename Fn> void with_cluster(Fn &&fn) {
        if (cluster) {
            fn();
        }
    }

    void set_main_window(main_window_t *window);
    main_window_t *get_main_window();
    void set_devices(tree_item_t *node);
    void set_folders(tree_item_t *node);
    void set_pending_devices(tree_item_t *node);
    void set_ignored_devices(tree_item_t *node);
    void set_show_deleted(bool value);
    void set_show_missing(bool value);
    void set_show_colorized(bool value);

    callback_ptr_t call_select_folder(std::string_view folder_id);
    callback_ptr_t call_share_folders(std::string_view folder_id, std::vector<utils::bytes_t> devices);
    db_info_viewer_guard_t request_db_info(db_info_viewer_t *viewer);
    void request_load_model();
    r::address_ptr_t &get_coordinator_address();

    std::uint32_t mask_nodes() const noexcept;

  private:
    using clock_t = std::chrono::high_resolution_clock;
    using time_point_t = typename clock_t::time_point;
    using callbacks_t = std::list<callback_ptr_t>;

    void on_model_response(model::message::model_response_t &res) noexcept;
    void on_model_update(model::message::model_update_t &message) noexcept;
    void on_db_info_response(net::message::db_info_response_t &res) noexcept;
    void redisplay_folder_nodes(bool refresh_labels);

    outcome::result<void> operator()(const model::diff::advance::advance_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::load::load_cluster_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::local::io_failure_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::modify::add_pending_folders_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::modify::add_pending_device_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::modify::add_ignored_device_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::modify::update_peer_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::modify::upsert_folder_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::modify::upsert_folder_info_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::peer::update_folder_t &, void *) noexcept override;

    outcome::result<void> apply(const model::diff::load::blocks_t &, model::cluster_t &cluster) noexcept override;
    outcome::result<void> apply(const model::diff::load::file_infos_t &, model::cluster_t &cluster) noexcept override;
    outcome::result<void> apply(const model::diff::load::load_cluster_t &, model::cluster_t &cluster) noexcept override;

    model::sequencer_ptr_t sequencer;
    time_point_t started_at;
    r::address_ptr_t coordinator;
    r::address_ptr_t sink;
    utils::logger_t log;
    dist_sink_t dist_sink;
    spdlog::sink_ptr ui_sink;
    bfs::path config_path;
    config::main_t app_config;
    config::main_t app_config_original;
    model::cluster_ptr_t cluster;
    content_t *content;
    tree_item_t *devices;
    tree_item_t *folders;
    tree_item_t *pending_devices;
    tree_item_t *ignored_devices;
    db_info_viewer_t *db_info_viewer;
    callbacks_t callbacks;
    main_window_t *main_window;
    std::size_t loaded_blocks;
    std::size_t loaded_files;
    const model::diff::load::load_cluster_t *load_cluster;

    friend struct db_info_viewer_guard_t;
};

} // namespace syncspirit::fltk


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024-2025 Ivan Baidakou

#include "augmentation.h"
#include "tree_item.h"

#include <boost/nowide/convert.hpp>

namespace syncspirit::fltk {

augmentation_t::augmentation_t(tree_item_t *owner_) : owner{owner_} {}

void augmentation_t::on_update() noexcept {
    if (owner) {
        owner->on_update();
    }
}

void augmentation_t::on_delete() noexcept {
    if (owner) {
        owner->on_delete();
    }
}

void augmentation_t::release_owner() noexcept { owner = nullptr; }

tree_item_t *augmentation_t::get_owner() noexcept { return owner; }

} // namespace syncspirit::fltk


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024-2025 Ivan Baidakou

#pragma once

#include "model/misc/augmentation.h"

namespace syncspirit::fltk {

struct tree_item_t;
struct dynamic_item_t;

struct augmentation_base_t : model::augmentation_t {
    virtual tree_item_t *get_owner() noexcept = 0;
    virtual void release_owner() noexcept = 0;
};

using augmentation_ptr_t = model::intrusive_ptr_t<augmentation_base_t>;

struct augmentation_t : augmentation_base_t {
    augmentation_t(tree_item_t *owner);

    void on_update() noexcept override;
    void on_delete() noexcept override;
    void release_owner() noexcept override;
    tree_item_t *get_owner() noexcept override;

  protected:
    tree_item_t *owner;
};

} // namespace syncspirit::fltk


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024-2025 Ivan Baidakou

#include "category.h"

#include "properties.h"

namespace syncspirit::fltk::config {

category_t::category_t(std::string label_, std::string explanation_, properties_t properties_)
    : label{std::move(label_)}, explanation{std::move(explanation_)}, properties{std::move(properties_)} {}

const std::string &category_t::get_label() const { return label; }
const std::string &category_t::get_explanation() const { return explanation; }
const properties_t &category_t::get_properties() const { return properties; }

auto reflect(const main_cfg_t &config, const main_cfg_t &default_config) -> categories_t {
    auto r = categories_t{};

    auto c_bep = [&]() -> category_ptr_t {
        auto &bep = config.bep_config;
        auto &bep_def = default_config.bep_config;
        auto props = properties_t{
            // clang-format off
            property_ptr_t(new bep::blocks_max_requested_t(bep.blocks_max_requested, bep_def.blocks_max_requested)),
            property_ptr_t(new bep::blocks_simultaneous_write_t(bep.blocks_simultaneous_write, bep_def.blocks_simultaneous_write)),
            property_ptr_t(new bep::connect_timeout_t(bep.connect_timeout, bep_def.connect_timeout)),
            property_ptr_t(new bep::advances_per_iteration_t(bep.advances_per_iteration, bep_def.advances_per_iteration)),
            property_ptr_t(new bep::request_timeout_t(bep.request_timeout, bep_def.request_timeout)),
            property_ptr_t(new bep::rx_buff_size_t(bep.rx_buff_size, bep_def.rx_buff_size)),
            property_ptr_t(new bep::rx_timeout_t(bep.rx_timeout, bep_def.rx_timeout)),
            property_ptr_t(new bep::tx_buff_limit_t(bep.tx_buff_limit, bep_def.tx_buff_limit)),
            property_ptr_t(new bep::tx_timeout_t(bep.tx_timeout, bep_def.tx_timeout)),
            property_ptr_t(new bep::stats_interval_t(bep.stats_interval, bep_def.stats_interval)),
            // clang-format on
        };
        return new category_t("bep", "BEP protocol/network settings", std::move(props));
    }();

    auto c_db = [&]() -> category_ptr_t {
        auto &db = config.db_config;
        auto &db_def = default_config.db_config;
        auto props = properties_t{
            // clang-format off
            property_ptr_t(new db::max_blocks_per_diff_t(db.max_blocks_per_diff, db_def.max_blocks_per_diff)),
            property_ptr_t(new db::max_files_per_diff_t(db.max_files_per_diff, db_def.max_files_per_diff)),
            property_ptr_t(new db::uncommitted_threshold_t(db.uncommitted_threshold, db_def.uncommitted_threshold)),
            property_ptr_t(new db::upper_limit_t(db.upper_limit, db_def.upper_limit)),
            // clang-format on
        };
        return new category_t("db", "database settings", std::move(props));
    }();

    auto c_dialer = [&]() -> category_ptr_t {
        auto &d = config.dialer_config;
        auto &d_def = default_config.dialer_config;
        auto props = properties_t{
            // clang-format off
            property_ptr_t(new dialer::enabled_t(d.enabled, d_def.enabled)),
            property_ptr_t(new dialer::redial_timeout_t(d.redial_timeout, d_def.redial_timeout)),
            property_ptr_t(new dialer::skip_discovers_t(d.skip_discovers, d_def.skip_discovers)),
            // clang-format on
        };
        return new category_t("dialer", "outbound connection scheduler settings", std::move(props));
    }();

    auto c_fs = [&]() -> category_ptr_t {
        auto &f = config.fs_config;
        auto &f_def = default_config.fs_config;
        auto props = properties_t{
            // clang-format off
            property_ptr_t(new fs::mru_size_t(f.mru_size, f_def.mru_size)),
            property_ptr_t(new fs::temporally_timeout_t(f.temporally_timeout, f_def.temporally_timeout)),
            property_ptr_t(new fs::bytes_scan_iteration_limit_t(f.bytes_scan_iteration_limit, f_def.bytes_scan_iteration_limit)),
            property_ptr_t(new fs::files_scan_iteration_limit_t(f.files_scan_iteration_limit, f_def.files_scan_iteration_limit)),
            // clang-format on
        };
        return new category_t("fs", "filesystem settings", std::move(props));
    }();

    auto c_gd = [&]() -> category_ptr_t {
        auto &g = config.global_announce_config;
        auto &g_def = default_config.global_announce_config;
        auto props = properties_t{
            // clang-format off
            property_ptr_t(new global_discovery::enabled_t(g.enabled, g_def.enabled)),
            property_ptr_t(new global_discovery::debug_t(g.debug, g_def.debug)),
            property_ptr_t(new global_discovery::cert_file_t(g.cert_file, g_def.cert_file)),
            property_ptr_t(new global_discovery::key_file_t(g.key_file, g_def.key_file)),
            property_ptr_t(new global_discovery::announce_url_t(g.announce_url->buffer(), g_def.announce_url->buffer())),
            property_ptr_t(new global_discovery::device_id_t(g.device_id, g_def.device_id)),
            property_ptr_t(new global_discovery::rx_buff_size_t(g.rx_buff_size, g_def.rx_buff_size)),
            property_ptr_t(new global_discovery::timeout_t(g.timeout, g_def.timeout)),
            // clang-format on
        };
        return new category_t("global_discovery", "global peer discovery settings", std::move(props));
    }();

    auto c_ld = [&]() -> category_ptr_t {
        auto &l = config.local_announce_config;
        auto &l_def = default_config.local_announce_config;
        auto props = properties_t{
            // clang-format off
            property_ptr_t(new local_discovery::enabled_t(l.enabled, l_def.enabled)),
            property_ptr_t(new local_discovery::frequency_t(l.frequency, l_def.frequency)),
            property_ptr_t(new local_discovery::port_t(l.port, l_def.port)),
            // clang-format on
        };
        return new category_t("local_discovery", "LAN peer discovery settings", std::move(props));
    }();

    auto c_main = [&]() -> category_ptr_t {
        auto &l = config;
        auto &l_def = default_config;
        auto props = properties_t{
            // clang-format off
            property_ptr_t(new main::default_location_t(l.default_location.string(), l_def.default_location.string())),
            property_ptr_t(new main::device_name_t(l.device_name, l_def.device_name)),
            property_ptr_t(new main::hasher_threads_t(l.hasher_threads, l_def.hasher_threads)),
            property_ptr_t(new main::timeout_t(l.timeout, l_def.timeout)),
            // clang-format on
        };
        return new category_t("main", "main application settings", std::move(props));
    }();

    auto c_relay = [&]() -> category_ptr_t {
        auto &l = config.relay_config;
        auto &l_def = default_config.relay_config;
        auto props = properties_t{
            // clang-format off
            property_ptr_t(new relay::enabled_t(l.enabled, l_def.enabled)),
            property_ptr_t(new relay::debug_t(l.enabled, l_def.enabled)),
            property_ptr_t(new relay::discovery_url_t(l.discovery_url->buffer(), l_def.discovery_url->buffer())),
            property_ptr_t(new relay::rx_buff_size_t(l.rx_buff_size, l_def.rx_buff_size)),
            // clang-format on
        };
        return new category_t("relay", "relay network/protocol settings", std::move(props));
    }();

    auto c_upnp = [&]() -> category_ptr_t {
        auto &l = config.upnp_config;
        auto &l_def = default_config.upnp_config;
        auto props = properties_t{
            // clang-format off
            property_ptr_t(new upnp::enabled_t(l.enabled, l_def.enabled)),
            property_ptr_t(new upnp::debug_t(l.debug, l_def.debug)),
            property_ptr_t(new upnp::external_port_t(l.external_port, l_def.external_port)),
            property_ptr_t(new upnp::max_wait_t(l.max_wait, l_def.max_wait)),
            property_ptr_t(new upnp::rx_buff_size_t(l.rx_buff_size, l_def.rx_buff_size)),
            // clang-format on
        };
        return new category_t("upnp", "universal plug and play router settings", std::move(props));
    }();

    auto c_logs = [&]() -> category_ptr_t {
        auto &sinks = config.log_configs;
        auto props = properties_t{};
        for (auto &s : sinks) {
            props.emplace_back(property_ptr_t(new impl::log_sink_t(s.name, s.sinks, s.level)));
        }
        return new category_t("logging", "log sinks & levels", std::move(props));
    }();

    r.push_back(std::move(c_bep));
    r.push_back(std::move(c_db));
    r.push_back(std::move(c_dialer));
    r.push_back(std::move(c_fs));
    r.push_back(std::move(c_gd));
    r.push_back(std::move(c_ld));
    r.push_back(std::move(c_main));
    r.push_back(std::move(c_relay));
    r.push_back(std::move(c_upnp));
    r.push_back(std::move(c_logs));

    return r;
}

auto reflect(const categories_t &categories, const main_cfg_t &default_config) -> main_cfg_t {
    main_cfg_t cfg = default_config;
    for (auto &c : categories) {
        for (auto &p : c->get_properties()) {
            p->reflect_to(cfg);
        }
    }
    return cfg;
}

bool is_valid(const categories_t &categories) {
    for (auto &c : categories) {
        for (auto &p : c->get_properties()) {
            if (auto &err = p->validate(); err) {
                return false;
            }
        }
    }
    return true;
}

} // namespace syncspirit::fltk::config


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024-2025 Ivan Baidakou

#pragma once

#include "property.h"
#include <vector>

namespace syncspirit::fltk::config {

using properties_t = std::vector<property_ptr_t>;

struct category_t : boost::intrusive_ref_counter<category_t, boost::thread_unsafe_counter> {
    using parent_t = boost::intrusive_ref_counter<category_t, boost::thread_unsafe_counter>;

    category_t(std::string label, std::string explanation, properties_t properties);
    virtual ~category_t() = default;
    const std::string &get_label() const;
    const std::string &get_explanation() const;
    const properties_t &get_properties() const;

  private:
    std::string label;
    std::string explanation;
    properties_t properties;
};

using category_ptr_t = boost::intrusive_ptr<category_t>;
using categories_t = std::vector<category_ptr_t>;

auto reflect(const main_cfg_t &config, const main_cfg_t &default_config) -> categories_t;
auto reflect(const categories_t &, const main_cfg_t &default_config) -> main_cfg_t;
bool is_valid(const categories_t &);

} // namespace syncspirit::fltk::config


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024-2025 Ivan Baidakou

#include "control.h"

#include "table.h"
#include "config/utils.h"
#include "../tree_item/settings.h"
#include <FL/Fl_Button.H>

namespace syncspirit::fltk::config {

static constexpr int PADDING = 5;

control_t::control_t(tree_item_t &tree_item_, int x, int y, int w, int h)
    : parent_t(x, y, w, h, "global app settings"), tree_item{tree_item_} {

    auto &sup = tree_item_.supervisor;
    auto config_path = sup.get_config_path();
    auto defaults_opt = syncspirit::config::generate_config(config_path);
    if (!defaults_opt) {
        auto ec = defaults_opt.assume_error();
        sup.get_logger()->error("cannot generate default config at {}: {}", config_path.string(), ec.message());
    } else {
        default_cfg = std::move(defaults_opt.assume_value());
        categories = reflect(sup.get_app_config(), default_cfg);
    }

    auto bottom_h = 40;
    auto table = new table_t(categories, x + PADDING, y + PADDING, w - PADDING * 2, h - (bottom_h + PADDING * 3));
    table->callback([](auto, void *data) { reinterpret_cast<control_t *>(data)->on_setting_modify(); }, this);

    auto bottom_group = new Fl_Group(x + PADDING, table->y() + table->h() + PADDING, w - PADDING * 2, bottom_h);
    bottom_group->begin();
    auto common_y = bottom_group->y() + PADDING;
    auto common_w = bottom_group->w() / 2 - PADDING;
    auto common_h = bottom_group->h() - PADDING * 2;
    auto save = new Fl_Button(bottom_group->x(), common_y, common_w, common_h, "save");
    save->callback([](Fl_Widget *, void *data) { reinterpret_cast<control_t *>(data)->on_save(); }, this);

    auto reset = new Fl_Button(save->x() + common_w + PADDING, common_y, common_w, common_h, "defaults");
    reset->callback([](Fl_Widget *, void *data) { reinterpret_cast<control_t *>(data)->on_reset(); }, this);

    bottom_group->end();

    resizable(table);
    box(FL_FLAT_BOX);
}

void control_t::on_reset() {
    for (auto &c : categories) {
        for (auto &p : c->get_properties()) {
            p->reset();
        }
    }
    auto table = static_cast<table_t *>(child(0));
    table->reload_values();
}

void control_t::on_setting_modify() {
    bool modified = false;
    for (size_t i = 0; i < categories.size() && !modified; ++i) {
        auto &c = categories[i];
        for (auto &p : c->get_properties()) {
            if (!p->same_as_initial()) {
                modified = true;
                break;
            }
        }
    }
    static_cast<tree_item::settings_t &>(tree_item).update_label(modified);
}

void control_t::on_save() {
    auto &sup = tree_item.supervisor;
    auto &log = sup.get_logger();
    if (!is_valid(categories)) {
        log->error("config has error, saving is not possible");
        return;
    }

    auto cfg = reflect(categories, default_cfg);
    cfg.config_path = sup.get_config_path();
    cfg.fltk_config = sup.get_app_config().fltk_config;

    sup.write_config(cfg);
}

} // namespace syncspirit::fltk::config


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024-2025 Ivan Baidakou

#pragma once

#include "../tree_item.h"
#include "category.h"
#include <FL/Fl_Group.H>

namespace syncspirit::fltk::config {

struct control_t : contentable_t<Fl_Group> {
    using parent_t = contentable_t<Fl_Group>;

    control_t(tree_item_t &tree_item, int x, int y, int w, int h);

    void on_reset();
    void on_save();
    void on_setting_modify();

    tree_item_t &tree_item;
    categories_t categories;
    main_cfg_t default_cfg;
};

} // namespace syncspirit::fltk::config


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024-2025 Ivan Baidakou

#include "properties.h"
#include "utils/log.h"
#include "model/device_id.h"
#include <charconv>
#include <boost/nowide/convert.hpp>
#include <filesystem>
#include <fmt/ranges.h>

namespace syncspirit::fltk::config {

namespace impl {

positive_integer_t::positive_integer_t(std::string label, std::string explanation, uint64_t value,
                                       uint64_t default_value)
    : property_t(std::move(label), std::move(explanation), std::to_string(value), std::to_string(default_value),
                 property_kind_t::positive_integer),
      native_value{value} {}

error_ptr_t positive_integer_t::validate_value() noexcept {
    std::uint64_t r;
    auto [ptr, ec] = std::from_chars(value.data(), value.data() + value.size(), r);

    if (ec == std::errc::invalid_argument) {
        return error_ptr_t(new std::string("not a number"));
    } else if (ec == std::errc::result_out_of_range) {
        return error_ptr_t(new std::string("too large number"));
    }
    assert(ec == std::errc());

    if (r <= 0) {
        return error_ptr_t(new std::string("not a positive number"));
    }

    // all ok
    native_value = static_cast<std::uint64_t>(r);
    return {};
}

string_t::string_t(std::string label, std::string explanation, std::string value, std::string default_value)
    : property_t(std::move(label), std::move(explanation), std::move(value), std::move(default_value),
                 property_kind_t::text) {}

error_ptr_t url_t::validate_value() noexcept {
    auto new_url = utils::parse(value);
    if (!new_url) {
        return error_ptr_t(new std::string("invalid url"));
    }
    auto original = utils::parse(default_value);
    if (new_url->scheme() != original->scheme()) {
        return error_ptr_t(new std::string("invalid protocol"));
    }
    return {};
}

path_t::path_t(std::string label, std::string explanation, std::string value, std::string default_value,
               property_kind_t kind)
    : property_t(std::move(label), std::move(explanation), std::move(value), std::move(default_value), kind) {}

bfs::path path_t::convert() noexcept { return bfs::path(boost::nowide::widen(value)); }

bool_t::bool_t(bool value, bool default_value, std::string label)
    : property_t(std::move(label), "", value ? "true" : "", default_value ? "true" : "", property_kind_t::boolean),
      native_value(value ? 1 : 0) {}

error_ptr_t bool_t::validate_value() noexcept {
    native_value = (value == "true") ? 1 : 0;
    return {};
}

static std::string join(const std::vector<std::string> &sinks) { return fmt::format("{}", fmt::join(sinks, ", ")); }

log_sink_t::log_sink_t(std::string name, std::vector<std::string> sinks_, spdlog::level::level_enum level_)
    : property_t(std::move(name), std::string(utils::get_level_string(level_)), join(sinks_), join(sinks_),
                 property_kind_t::log_sink),
      sinks(std::move(sinks_)), level{level_} {}

void log_sink_t::reflect_to(syncspirit::config::main_t &main) {
    main.log_configs.clear();
    main.log_configs.push_back(syncspirit::config::log_config_t{
        label,
        level,
        sinks,
    });
}

} // namespace impl

namespace bep {

blocks_max_requested_t::blocks_max_requested_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("blocks_max_requested", explanation_, value, default_value) {}

void blocks_max_requested_t::reflect_to(syncspirit::config::main_t &main) {
    main.bep_config.blocks_max_requested = native_value;
}

const char *blocks_max_requested_t::explanation_ = "maximum concurrent block read requests per peer";

blocks_simultaneous_write_t::blocks_simultaneous_write_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("blocks_simultaneous_write", explanation_, value, default_value) {}

void blocks_simultaneous_write_t::reflect_to(syncspirit::config::main_t &main) {
    main.bep_config.blocks_simultaneous_write = native_value;
}

const char *blocks_simultaneous_write_t::explanation_ = "maximum concurrent block write requests to disk";

connect_timeout_t::connect_timeout_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("connect_timeout", explanation_, value, default_value) {}

void connect_timeout_t::reflect_to(syncspirit::config::main_t &main) { main.bep_config.connect_timeout = native_value; }

const char *connect_timeout_t::explanation_ = "maximum time for connection, milliseconds";

advances_per_iteration_t::advances_per_iteration_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("advances_per_iteration", explanation_, value, default_value) {}

void advances_per_iteration_t::reflect_to(syncspirit::config::main_t &main) {
    main.bep_config.advances_per_iteration = native_value;
}

const char *advances_per_iteration_t::explanation_ = "maximum amount of file metadata advances per iteration";

request_timeout_t::request_timeout_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("request_timeout", explanation_, value, default_value) {}

void request_timeout_t::reflect_to(syncspirit::config::main_t &main) { main.bep_config.request_timeout = native_value; }

const char *request_timeout_t::explanation_ = "maximum time for request, milliseconds";

rx_buff_size_t::rx_buff_size_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("rx_buff_size", explanation_, value, default_value) {}

void rx_buff_size_t::reflect_to(syncspirit::config::main_t &main) { main.bep_config.rx_buff_size = native_value; }

const char *rx_buff_size_t::explanation_ = "preallocated receive buffer size, bytes";

rx_timeout_t::rx_timeout_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("rx_timeout", explanation_, value, default_value) {}

void rx_timeout_t::reflect_to(syncspirit::config::main_t &main) { main.bep_config.rx_timeout = native_value; }

const char *rx_timeout_t::explanation_ = "rx max time, milliseconds";

tx_buff_limit_t::tx_buff_limit_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("tx_buff_limit", explanation_, value, default_value) {}

void tx_buff_limit_t::reflect_to(syncspirit::config::main_t &main) { main.bep_config.tx_buff_limit = native_value; }

const char *tx_buff_limit_t::explanation_ = "preallocated transmit buffer size";

tx_timeout_t::tx_timeout_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("tx_timeout", explanation_, value, default_value) {}

void tx_timeout_t::reflect_to(syncspirit::config::main_t &main) { main.bep_config.tx_timeout = native_value; }

const char *tx_timeout_t::explanation_ = "tx max time, milliseconds";

stats_interval_t::stats_interval_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("tx_timeout", explanation_, value, default_value) {}

void stats_interval_t::reflect_to(syncspirit::config::main_t &main) { main.bep_config.stats_interval = native_value; }

const char *stats_interval_t::explanation_ = "min delay before gathering I/O stats, milliseconds";

} // namespace bep

namespace db {

max_blocks_per_diff_t::max_blocks_per_diff_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("max_blocks_per_diff", explanation_, value, default_value) {}

void max_blocks_per_diff_t::reflect_to(syncspirit::config::main_t &main) {
    main.db_config.max_blocks_per_diff = native_value;
}

const char *max_blocks_per_diff_t::explanation_ =
    "maximum number of blocks per single diff (to display progress in UI)";

max_files_per_diff_t::max_files_per_diff_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("max_files_per_diff", explanation_, value, default_value) {}

void max_files_per_diff_t::reflect_to(syncspirit::config::main_t &main) {
    main.db_config.max_files_per_diff = native_value;
}

const char *max_files_per_diff_t::explanation_ = "maximum number of files per single diff (to display progress in UI)";

uncommitted_threshold_t::uncommitted_threshold_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("uncommitted_threshold", explanation_, value, default_value) {}

void uncommitted_threshold_t::reflect_to(syncspirit::config::main_t &main) {
    main.db_config.uncommitted_threshold = native_value;
}

const char *uncommitted_threshold_t::explanation_ = "how many transactions keep in memory before flushing to storage";

upper_limit_t::upper_limit_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("upper_limit", explanation_, value, default_value) {}

void upper_limit_t::reflect_to(syncspirit::config::main_t &main) { main.db_config.upper_limit = native_value; }

const char *upper_limit_t::explanation_ = "maximum database size, in bytes";

} // namespace db

namespace dialer {

void enabled_t::reflect_to(syncspirit::config::main_t &main) { main.dialer_config.enabled = native_value; }

redial_timeout_t::redial_timeout_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("redial_timeout", explanation_, value, default_value) {}

void redial_timeout_t::reflect_to(syncspirit::config::main_t &main) {
    main.dialer_config.redial_timeout = native_value;
}

const char *redial_timeout_t::explanation_ = "how often try to redial to connect offline peers, milliseconds";

skip_discovers_t::skip_discovers_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("skip_discovers", explanation_, value, default_value) {}

void skip_discovers_t::reflect_to(syncspirit::config::main_t &main) {
    main.dialer_config.skip_discovers = native_value;
}

const char *skip_discovers_t::explanation_ = "when peer addresses are known, how many times skip rediscovering them";

} // namespace dialer

namespace fs {

mru_size_t::mru_size_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("mru_size", explanation_, value, default_value) {}

void mru_size_t::reflect_to(syncspirit::config::main_t &main) { main.fs_config.mru_size = native_value; }

const char *mru_size_t::explanation_ = "maximum amount of cached/opened files";

temporally_timeout_t::temporally_timeout_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("temporally_timeout", explanation_, value, default_value) {}

void temporally_timeout_t::reflect_to(syncspirit::config::main_t &main) {
    main.fs_config.temporally_timeout = native_value;
}

const char *temporally_timeout_t::explanation_ = "remove incomplete file after this amount of seconds";

bytes_scan_iteration_limit_t::bytes_scan_iteration_limit_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("bytes_scan_iteration_limit", explanation_, value, default_value) {}

void bytes_scan_iteration_limit_t::reflect_to(syncspirit::config::main_t &main) {
    main.fs_config.bytes_scan_iteration_limit = native_value;
}

const char *bytes_scan_iteration_limit_t::explanation_ = "max number of bytes before emitting scan events";

files_scan_iteration_limit_t::files_scan_iteration_limit_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("files_scan_iteration_limit", explanation_, value, default_value) {}

void files_scan_iteration_limit_t::reflect_to(syncspirit::config::main_t &main) {
    main.fs_config.files_scan_iteration_limit = native_value;
}

const char *files_scan_iteration_limit_t::explanation_ = "max number processed files before emitting scan events";

} // namespace fs

namespace global_discovery {

void enabled_t::reflect_to(syncspirit::config::main_t &main) { main.global_announce_config.enabled = native_value; }

debug_t::debug_t(bool value, bool default_value) : parent_t(value, default_value, "debug") {}

void debug_t::reflect_to(syncspirit::config::main_t &main) { main.global_announce_config.debug = native_value; }

announce_url_t::announce_url_t(std::string value, std::string default_value)
    : parent_t("announce_url", explanation_, std::move(value), std::move(default_value)) {}

void announce_url_t::reflect_to(syncspirit::config::main_t &main) {
    main.global_announce_config.announce_url = utils::parse(value);
}

const char *announce_url_t::explanation_ = "url of syncthing/private global announce server";

cert_file_t::cert_file_t(std::string value, std::string default_value)
    : parent_t("cert_file", explanation_, std::move(value), std::move(default_value)) {}

void cert_file_t::reflect_to(syncspirit::config::main_t &main) { main.global_announce_config.cert_file = value; }

const char *cert_file_t::explanation_ = "this device certificate location";

device_id_t::device_id_t(std::string value, std::string default_value)
    : parent_t("device_id", explanation_, std::move(value), std::move(default_value)) {}

void device_id_t::reflect_to(syncspirit::config::main_t &main) { main.global_announce_config.device_id = value; }

error_ptr_t device_id_t::validate_value() noexcept {
    auto opt = model::device_id_t::from_string(value);
    if (!opt) {
        return error_ptr_t(new std::string("invalid device id"));
    }
    return {};
}

const char *device_id_t::explanation_ = "device_id of global/private discovery server";

key_file_t::key_file_t(std::string value, std::string default_value)
    : parent_t("key_file", explanation_, std::move(value), std::move(default_value)) {}

void key_file_t::reflect_to(syncspirit::config::main_t &main) { main.global_announce_config.key_file = value; }

const char *key_file_t::explanation_ = "this device key location";

rx_buff_size_t::rx_buff_size_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("rx_buff_size", explanation_, value, default_value) {}

void rx_buff_size_t::reflect_to(syncspirit::config::main_t &main) {
    main.global_announce_config.rx_buff_size = native_value;
}

const char *rx_buff_size_t::explanation_ = "preallocated receive buffer size, bytes";

timeout_t::timeout_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("timeout", explanation_, value, default_value) {}

void timeout_t::reflect_to(syncspirit::config::main_t &main) { main.global_announce_config.timeout = native_value; }

const char *timeout_t::explanation_ = "max request/response time, milliseconds";

} // namespace global_discovery

namespace local_discovery {

void enabled_t::reflect_to(syncspirit::config::main_t &main) { main.local_announce_config.enabled = native_value; }

frequency_t::frequency_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("frequency", explanation_, value, default_value) {}

void frequency_t::reflect_to(syncspirit::config::main_t &main) { main.local_announce_config.frequency = native_value; }

const char *frequency_t::explanation_ = "how often send announcements in LAN, in milliseconds, milliseconds";

port_t::port_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("port", explanation_, value, default_value) {}

void port_t::reflect_to(syncspirit::config::main_t &main) { main.local_announce_config.port = native_value; }

const char *port_t::explanation_ = "upd port used for announcement (should be the same as in syncthing)";

} // namespace local_discovery

namespace main {

default_location_t::default_location_t(std::string value, std::string default_value)
    : parent_t("default_location", explanation_, std::move(value), std::move(default_value),
               property_kind_t::directory) {}

void default_location_t::reflect_to(syncspirit::config::main_t &main) { main.default_location = convert(); }

const char *default_location_t::explanation_ = "where folders are created by default";

device_name_t::device_name_t(std::string value, std::string default_value)
    : parent_t("device_name", explanation_, std::move(value), std::move(default_value)) {}

void device_name_t::reflect_to(syncspirit::config::main_t &main) { main.device_name = value; }

const char *device_name_t::explanation_ = "this device name";

hasher_threads_t::hasher_threads_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("hasher_threads", explanation_, value, default_value) {}

void hasher_threads_t::reflect_to(syncspirit::config::main_t &main) { main.hasher_threads = native_value; }

const char *hasher_threads_t::explanation_ = "amount cpu cores used for hashing";

timeout_t::timeout_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("timeout", explanation_, value, default_value) {}

void timeout_t::reflect_to(syncspirit::config::main_t &main) { main.timeout = native_value; }

const char *timeout_t::explanation_ = "main actors timeout, milliseconds";

} // namespace main

namespace relay {

void enabled_t::reflect_to(syncspirit::config::main_t &main) { main.relay_config.enabled = native_value; }

debug_t::debug_t(bool value, bool default_value) : parent_t(value, default_value, "debug") {}

void debug_t::reflect_to(syncspirit::config::main_t &main) { main.relay_config.debug = native_value; }

discovery_url_t::discovery_url_t(std::string value, std::string default_value)
    : parent_t("discovery_url", explanation_, std::move(value), std::move(default_value)) {}

void discovery_url_t::reflect_to(syncspirit::config::main_t &main) {
    main.relay_config.discovery_url = utils::parse(value);
}

const char *discovery_url_t::explanation_ = "where pick the list of relay servers pool";

rx_buff_size_t::rx_buff_size_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("rx_buff_size", explanation_, value, default_value) {}

void rx_buff_size_t::reflect_to(syncspirit::config::main_t &main) { main.relay_config.rx_buff_size = native_value; }

const char *rx_buff_size_t::explanation_ = "preallocated receive buffer size, bytes";

} // namespace relay

namespace upnp {

void enabled_t::reflect_to(syncspirit::config::main_t &main) { main.upnp_config.enabled = native_value; }

debug_t::debug_t(bool value, bool default_value) : parent_t(value, default_value, "debug") {}

void debug_t::reflect_to(syncspirit::config::main_t &main) { main.upnp_config.debug = native_value; }

external_port_t::external_port_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("external_port", explanation_, value, default_value) {}

void external_port_t::reflect_to(syncspirit::config::main_t &main) { main.upnp_config.external_port = native_value; }

const char *external_port_t::explanation_ = "external (router) port for communication, opened on router";

max_wait_t::max_wait_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("max_wait", explanation_, value, default_value) {}

void max_wait_t::reflect_to(syncspirit::config::main_t &main) { main.upnp_config.max_wait = native_value; }

const char *max_wait_t::explanation_ = "router response max wait time, seconds";

rx_buff_size_t::rx_buff_size_t(std::uint64_t value, std::uint64_t default_value)
    : parent_t("rx_buff_size", explanation_, value, default_value) {}

void rx_buff_size_t::reflect_to(syncspirit::config::main_t &main) { main.upnp_config.rx_buff_size = native_value; }

const char *rx_buff_size_t::explanation_ = "preallocated receive buffer size, bytes";

} // namespace upnp

} // namespace syncspirit::fltk::config