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

#include "log_table.h"
#include "log_sink.h"
#include "log_colors.h"
#include "log_utils.h"

#include <FL/Fl.H>
#include <FL/fl_draw.H>

#include <spdlog/sinks/sink.h>
#include <spdlog/pattern_formatter.h>
#include <spdlog/fmt/fmt.h>
#include <sstream>

namespace syncspirit::fltk {

static constexpr int col_min_size = 60;

log_table_t::log_table_t(log_buffer_ptr_t &displayed_records_, int x, int y, int w, int h)
    : parent_t(x, y, w, h), displayed_records{displayed_records_}, auto_scrolling(true) {
    rows(0);            // how many rows
    row_header(0);      // enable row headers (along left)
    row_height_all(20); // default height of rows
    row_resize(0);      // disable row resizing
    // Cols
    cols(4);       // how many columns
    col_header(1); // enable column headers (along top)
    col_resize_min(col_min_size);
    col_width(0, 170);
    col_width(1, 100);
    col_width(2, 220);

    auto message_col_sz = this->w() - (col_width(0) + col_width(1) + col_width(2));
    col_width(3, message_col_sz);

    col_resize(1); // enable column resizing
    end();         // end the Fl_Table group

    // receive resize events
    when(FL_WHEN_CHANGED | when());
    set_visible_focus();
}

void log_table_t::update() {
    rows(displayed_records->size());
    if (auto_scrolling) {
        row_position(static_cast<int>(displayed_records->size()));
    }
}

void log_table_t::draw_cell(TableContext context, int row, int col, int x, int y, int w, int h) {
    switch (context) {
    case CONTEXT_STARTPAGE:        // before page is drawn..
        fl_font(FL_HELVETICA, 16); // set the font for our drawing operations
        return;
    case CONTEXT_COL_HEADER: // Draw column headers
        draw_header(col, x, y, w, h);
        return;
    case CONTEXT_CELL: // Draw data in cells
        draw_data(row, col, x, y, w, h);
        return;
    case CONTEXT_RC_RESIZE: {
        auto until_last = col_width(0) + col_width(1) + col_width(2);
        auto last_sz = this->tiw - until_last;
        if (last_sz >= col_min_size) {
            col_width(3, last_sz);
        }
        return;
    }
    default:
        return;
    }
}

void log_table_t::draw_header(int col, int x, int y, int w, int h) {
    std::string_view label = col == 0 ? "time" : col == 1 ? "thread" : col == 2 ? "source" : "message";
    fl_push_clip(x, y, w, h);
    {
        fl_draw_box(FL_THIN_UP_BOX, x, y, w, h, row_header_color());
        fl_color(FL_BLACK);
        fl_draw(label.data(), x, y, w, h, FL_ALIGN_CENTER);
    }
    fl_pop_clip();
}

void log_table_t::draw_data(int row, int col, int x, int y, int w, int h) {
    auto &record = *displayed_records->at(static_cast<size_t>(row));
    std::string *content;
    fl_push_clip(x, y, w, h);
    {
        Fl_Align align = FL_ALIGN_CENTER;
        int x_offset = 0;
        int w_adj = 0;
        switch (col) {
        case 0:
            content = &record.date;
            break;
        case 1:
            content = &record.thread_id;
            align = FL_ALIGN_RIGHT;
            w_adj = 10;
            break;
        case 2:
            content = &record.source;
            break;
        default:
            content = &record.message;
            align = FL_ALIGN_LEFT;
            x_offset = 10;
            break;
        }

        auto color_row = row % 2;
        auto color_index = static_cast<int>(record.level) * 2;

        fl_color(row_selected(row) ? table_selection_color : log_colors[color_index + color_row]);
        fl_rectf(x, y, w, h);
        fl_color(FL_GRAY0);
        fl_draw(content->data(), x + x_offset, y, w - x_offset - w_adj, h, align);
        fl_color(color());
        fl_rect(x, y, w, h);
    }
    fl_pop_clip();
}

void log_table_t::autoscroll(bool value) {
    auto_scrolling = value;
    if (auto_scrolling) {
        row_position(static_cast<int>(displayed_records->size()));
    }
}

std::string log_table_t::gather_selected() {
    std::stringstream buff;
    size_t count = 0;
    for (size_t i = 0; i < displayed_records->size(); ++i) {
        if (row_selected(static_cast<int>(i))) {
            if (count) {
                buff << eol;
            };
            ++count;
            auto &row = displayed_records->at(i);
            buff << "(" << static_cast<int>(row->level) << ")"
                 << "\t" << row->date << "\t" << row->thread_id << "\t" << row->source << "\t" << row->message;
            ;
        }
    }
    return buff.str();
}

int log_table_t::handle(int event) {
    auto r = parent_t::handle(event);
    if (event == FL_RELEASE && r) {
        auto buff = gather_selected();
        if (buff.size()) {
            Fl::copy(buff.data(), buff.size(), 0);
        }
        take_focus();
    } else if (event == FL_KEYBOARD) {
        if ((Fl::event_state() & (FL_CTRL | FL_COMMAND)) && Fl::event_key() == 'c') {
            auto buff = gather_selected();
            if (buff.size()) {
                Fl::copy(buff.data(), buff.size(), 1);
            }
            r = 1;
        }
    } else if (event == FL_UNFOCUS) {
        selected_records.clear();
        for (size_t i = 0; i < displayed_records->size(); ++i) {
            if (row_selected((int)i)) {
                selected_records.push_back(i);
            }
        }
        select_all_rows(0);
        r = 1;
    }

    return r;
}

log_iterator_ptr_t log_table_t::get_selected() {
    struct it_t final : logs_iterator_t {
        it_t(selected_indices_t selected_records_, log_buffer_ptr_t &displayed_records_)
            : selected_records{std::move(selected_records_)}, displayed_records{displayed_records_}, i{0} {}

        log_record_t *next() override {
            if (i < selected_records.size()) {
                auto idx = selected_records[i++];
                return (*displayed_records)[idx].get();
            }
            return {};
        }

        selected_indices_t selected_records;
        log_buffer_ptr_t &displayed_records;
        size_t i;
    };

    if (selected_records.size()) {
        return log_iterator_ptr_t(new it_t(std::move(selected_records), displayed_records));
    }
    return {};
}

} // namespace syncspirit::fltk


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

#pragma once

#include "log_utils.h"
#include <FL/Fl_Table_Row.H>
#include <boost/circular_buffer.hpp>

namespace syncspirit::fltk {

struct fltk_sink_t;

struct log_table_t : Fl_Table_Row {
    using parent_t = Fl_Table_Row;

    log_table_t(log_buffer_ptr_t &displayed_records, int x, int y, int w, int h);

    void draw_cell(TableContext context, int row, int col, int x, int y, int w, int h) override;
    void autoscroll(bool value);
    void update();
    int handle(int event) override;
    log_iterator_ptr_t get_selected();

  private:
    using selected_indices_t = std::vector<std::size_t>;
    std::string gather_selected();
    void draw_header(int col, int x, int y, int w, int h);
    void draw_data(int row, int col, int x, int y, int w, int h);

    selected_indices_t selected_records;
    log_buffer_ptr_t &displayed_records;
    bool auto_scrolling;
};

} // namespace syncspirit::fltk

repo_name: basiliscos/syncspirit

license: mit


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

#include "log_utils.h"

namespace syncspirit::fltk {

const char *eol =
#ifdef _WIN32
    "\r\n"
#else
    "\n"
#endif
    ;

} // namespace syncspirit::fltk


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

#pragma once

#include <string>
#include <vector>
#include <memory>

#include <boost/intrusive_ptr.hpp>
#include <boost/smart_ptr/intrusive_ref_counter.hpp>
#include <boost/circular_buffer.hpp>
#include <spdlog/spdlog.h>

namespace syncspirit::fltk {

struct log_record_t : boost::intrusive_ref_counter<log_record_t, boost::thread_unsafe_counter> {
    log_record_t(spdlog::level::level_enum level_, std::string date_, std::string source_, std::string message_,
                 std::string thread_id_)
        : level{level_}, date{std::move(date_)}, source{std::move(source_)}, message{std::move(message_)},
          thread_id{std::move(thread_id_)} {}

    spdlog::level::level_enum level;
    std::string date;
    std::string source;
    std::string message;
    std::string thread_id;
};

struct logs_iterator_t {
    virtual ~logs_iterator_t() = default;
    virtual log_record_t *next() = 0;
};

using log_record_ptr_t = boost::intrusive_ptr<log_record_t>;
using log_buffer_t = boost::circular_buffer<log_record_ptr_t>;
using log_buffer_ptr_t = std::unique_ptr<log_buffer_t>;
using log_queue_t = std::vector<log_record_ptr_t>;
using log_iterator_ptr_t = std::unique_ptr<logs_iterator_t>;

extern const char *eol;

} // namespace syncspirit::fltk


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024-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 <exception>
#include <cstdlib>
#include <spdlog/sinks/stdout_color_sinks.h>

#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 "hasher/hasher_supervisor.h"
#include "net/net_supervisor.h"
#include "fs/fs_supervisor.h"

#include <FL/Fl.H>
#include <FL/Fl_Window.H>
#include <FL/Fl_Box.H>
#include <FL/fl_utf8.h>

#include "app_supervisor.h"
#include "main_window.h"
#include "log_sink.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 rf = rotor::fltk;
namespace asio = boost::asio;

using namespace syncspirit;

[[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);
    }
};

struct fltk_context_t : rf::system_context_fltk_t {
    using parent_t = rf::system_context_fltk_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

#ifdef _WIN32
#define SET_THREAD_EN_LANGUAGE() SetThreadUILanguage(MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US))
#else
#define SET_THREAD_EN_LANGUAGE()
#endif

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

    Fl::lock();

#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 {
        SET_THREAD_EN_LANGUAGE();
        utils::platform_t::startup();

        auto dist_sink = utils::create_root_logger();
        dist_sink->add_sink(spdlog::sink_ptr(new fltk::im_memory_sink_t()));
        if (auto value = std::getenv(constants::console_sink_env); value && value == std::string_view("1")) {
            auto console_sink = spdlog::sink_ptr(new spdlog::sinks::stderr_color_sink_mt());
            dist_sink->add_sink(console_sink);
        }
        spdlog::trace("root logger has been bootstrapped");

        Fl::args(1, argv);

#if defined(__linux__)
        pthread_setname_np(pthread_self(), "ss/main");
#endif
        // clang-format off
        /* parse command-line & config options */
        po::options_description cmdline_descr("Allowed options");
        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");
        // 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");
        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 :: {}", 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.string());
            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.string(), cfg_option.error());
            return 1;
        }
        auto &cfg = cfg_option.value();
        spdlog::trace("configuration seems OK, timeout = {}ms", cfg.timeout);

        // 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;
        }

        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;
            }
        }

        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 = 2ul;
        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();
        // warm-up
        sup_net->do_process();
        if (sup_net->get_shutdown_reason()) {
            spdlog::debug("net supervisor has not started");
            return 1;
        }

        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();
            auto sup = ctx->create_supervisor<hasher::hasher_supervisor_t>()
                           .timeout(timeout / 2)
                           .registry_address(sup_net->get_registry_address())
                           .index(i)
                           .finish();
            sup->do_process();
        }

        // window should outlive fltk ctx, as in ctx d-tor model augmentations
        // invoke fltk-things..
        auto main_window = std::unique_ptr<fltk::main_window_t>();
        auto fltk_ctx = rf::system_context_ptr_t(new fltk_context_t());
        auto sup_fltk = fltk_ctx->create_supervisor<fltk::app_supervisor_t>()
                            .dist_sink(bootstrap_guard->get_dist_sink())
                            .config_path(config_file_path)
                            .app_config(cfg)
                            .timeout(timeout)
                            .registry_address(sup_net->get_registry_address())
                            .shutdown_flag(shutdown_flag, r::pt::millisec{50})
                            .finish();
        // warm-up
        sup_fltk->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();
        fs_sup->do_process();

        auto app_w = cfg.fltk_config.main_window_width;
        auto app_h = cfg.fltk_config.main_window_height;
        main_window.reset(new fltk::main_window_t(*sup_fltk, app_w, app_h));
        Fl::visual(FL_DOUBLE | FL_INDEX);
        main_window->show(argc, argv);
        main_window->wait_for_expose();

        // launch
        auto net_thread = std::thread([&]() {
            SET_THREAD_EN_LANGUAGE();
#if defined(__linux__)
            std::string name = "ss/net";
            pthread_setname_np(pthread_self(), name.c_str());
#endif
            io_context.run();
            shutdown_flag = true;
            spdlog::trace("net 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]() {
                SET_THREAD_EN_LANGUAGE();
                std::string name = "ss/hasher-" + std::to_string(i + 1);
#if defined(__linux__)
                pthread_setname_np(pthread_self(), name.c_str());
#endif
                ctx->run();
                shutdown_flag = true;
                spdlog::trace("{} thread has been terminated", name);
            });
            hasher_threads.emplace_back(std::move(thread));
        }

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

        spdlog::debug("utf8 local support: {}", fl_utf8locale());

        while (!shutdown_flag) {
            sup_fltk->do_process();
            if (!Fl::wait()) {
                shutdown_flag = true;
                spdlog::debug("main window is longer show, terminating...");
                break;
            }
        }

        sup_fltk->do_shutdown();
        sup_fltk->do_process();

        fs_thread.join();
        net_thread.join();

        spdlog::trace("waiting hasher threads termination");
        for (auto &thread : hasher_threads) {
            thread.join();
        }

        if (auto reason = sup_net->get_shutdown_reason(); reason && reason->ec) {
            spdlog::info("app shut down reason: {}", reason->message());
        }

        spdlog::trace("everything has been terminated");
        sup_fltk.reset();
        fltk_ctx.reset();
        main_window.reset();
        spdlog::trace("fltk context has been destroyed");
    } catch (...) {
        spdlog::critical("unknown exception");
        // spdlog::critical("Starting failure : {}", ex.what());
        return 1;
    }

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

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


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

#include "main_window.h"

#include "app.h"
#include "log_panel.h"
#include "tree_view.h"
#include "tree_item.h"
#include "toolbar.h"

#include <FL/Fl_Box.H>
#include <FL/Fl_Tile.H>

using namespace syncspirit::fltk;

main_window_t::main_window_t(app_supervisor_t &supervisor_, int w_, int h_)
    : parent_t(w_, h_, "syncspirit-fltk"), supervisor{&supervisor_} {

#ifdef _WIN32
    auto icon = LoadIcon(fl_display, MAKEINTRESOURCE(ID_SYNCSPIRIT_ICON));
    this->icon((const void *)icon);
#endif
    supervisor->set_main_window(this);

    auto container = new Fl_Tile(0, 0, w(), h());
    auto &cfg = supervisor->get_app_config().fltk_config;
    auto left_share = std::min(std::max(0.1, cfg.left_panel_share), 0.9);
    auto bottom_share = std::min(std::max(0.1, cfg.bottom_panel_share), 0.9);
    container->begin();

    auto resizable_area = new Fl_Box(w() * 0.1, h() * 0.1, w() * 0.9, h() * 0.8);

    auto left_w = static_cast<int>(w() * left_share);
    auto right_w = w() - left_w;
    auto top_h = static_cast<int>(h() * (1 - bottom_share));
    auto bottom_h = h() - top_h;
    content_left = new Fl_Group(0, 0, left_w, top_h);
    content_left->box(FL_ENGRAVED_BOX);
    content_left->begin();
    auto toolbar = new toolbar_t(*supervisor, 0, 0, left_w, 0);
    toolbar->end();
    auto toolbar_h = toolbar->h();
    tree = new tree_view_t(*supervisor, 0, toolbar_h, left_w, top_h - toolbar_h);
    content_left->end();
    content_left->resizable(tree);

    supervisor->replace_content([&](content_t *) -> content_t * {
        struct my_box_t : contentable_t<Fl_Box> {
            using parent_t = contentable_t<Fl_Box>;
            using parent_t::parent_t;
            void refresh() override {}
        };

        auto box = new my_box_t(left_w, 0, right_w, top_h, "...");
        box->box(FL_ENGRAVED_BOX);
        return box;
    });

    log_panel = new log_panel_t(*supervisor, 0, 0, w(), bottom_h);
    log_panel->position(0, content_left->h());
    log_panel->box(FL_FLAT_BOX);

    container->end();
    container->resizable(resizable_area);

    end();

    resizable(this);
    deactivate();
}

main_window_t::~main_window_t() {
    // one of the child d-tors use `main_window` object (indirectly),
    // hence it should be alive a little bit before children deletion
    clear();
}

void main_window_t::on_shutdown() {
    auto &cfg = supervisor->get_app_config().fltk_config;
    cfg.main_window_height = h();
    cfg.main_window_width = w();
    cfg.left_panel_share = (content_left->w() + 0.) / w();
    cfg.bottom_panel_share = (log_panel->h() + 0.) / h();

    for (auto *item = tree->first(); item; item = tree->next(item)) {
        if (auto tree_item = dynamic_cast<tree_item_t *>(item); tree_item) {
            if (auto augmentation = tree_item->get_proxy(); augmentation) {
                auto aug = dynamic_cast<augmentation_base_t *>(augmentation.get());
                if (aug) {
                    aug->release_owner();
                }
            }
        }
    }
}

void main_window_t::set_splash_text(std::string text) {
    log_panel->set_splash_text(std::move(text));
    Fl::flush();
}

void main_window_t::on_loading_done() {
    log_panel->on_loading_done();
    activate();
}

void main_window_t::detach_supervisor() { supervisor = nullptr; }

app_supervisor_t *main_window_t::get_supervisor() { return supervisor; }


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

#pragma once

#include "app_supervisor.h"
#include <FL/Fl_Double_Window.H>
#include <string>

namespace syncspirit::fltk {

struct log_panel_t;
struct tree_view_t;

struct main_window_t : Fl_Double_Window {
    using parent_t = Fl_Double_Window;

    main_window_t(app_supervisor_t &supervisor, int w, int h);
    ~main_window_t();

    void on_shutdown();
    void set_splash_text(std::string text);
    void on_loading_done();
    void detach_supervisor();
    app_supervisor_t *get_supervisor();

  private:
    app_supervisor_t *supervisor;
    Fl_Group *content_left;
    tree_view_t *tree;
    log_panel_t *log_panel;
};

} // namespace syncspirit::fltk


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

#include "presence_item.h"
#include "presence_item/cluster.h"
#include "presence_item/missing.h"
#include "presentation/folder_presence.h"
#include "symbols.h"

#include <fmt/format.h>
#include <memory_resource>
#include <iterator>

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

using F = presence_t::features_t;

inline static bool is_hidden(presence_t *presence, std::uint32_t hide_mask) {
    auto features = presence->get_features();
    auto hide = features & hide_mask;

    // don't show missing & deleted
    if (!hide && features & F::missing) {
        auto best = presence->get_entity()->get_best();
        if (best) {
            hide = best->get_features() & hide_mask;
        }
    }
    return hide;
}

static auto make_item(presence_item_t *parent, presence_t &presence) -> presence_item_ptr_t {
    if (auto &aug = presence.get_augmentation(); aug) {
        return static_cast<presence_item_t *>(aug.get());
    }
    auto f = presence.get_features();
    if (f & F::cluster) {
        if ((f & F::directory) || (f & F::file)) {
            return new cluster_t(presence, parent->supervisor, parent->tree());
        }
    } else if (f & F::missing) {
        return new missing_t(parent, presence);
    }
    return {};
}

static auto get_host(presence_item_t *self) -> presence_item_t * {
    auto presence = &self->get_presence();
    auto parent = presence->get_parent();
    if (parent) {
        return static_cast<presence_item_t *>(parent->get_augmentation().get());
    } else if (presence->get_features() & F::missing) {
        return static_cast<missing_t *>(self)->host;
    }
    return nullptr;
}

presence_item_t::presence_item_t(presence_t &presence_, app_supervisor_t &supervisor, Fl_Tree *tree, bool augment)
    : parent_t(supervisor, tree, false), presence{&presence_}, expanded{false} {
    if (augment) {
        assert(!presence->get_augmentation());
        presence->set_augmentation(*this);
    }
}

presence_item_t::~presence_item_t() {
    auto p = parent();
    if (p) {
        auto self = do_hide();
        self.detach();
    }

    // will be de-allocated by augmentation, not by fltk
    auto child_count = children();
    while (children() > 0) {
        safe_detach(0);
    }
}

void presence_item_t::on_open() {
    if (expanded || !children()) {
        return;
    }

    safe_detach(0);
    expanded = true;

    int position = 0;
    auto hide_mask = supervisor.mask_nodes();
    for (auto c : presence->get_children()) {
        if (!is_hidden(c, hide_mask)) {
            auto node = make_item(this, *c);
            if (node) {
                insert_node(node, position++);
                node->show(hide_mask, false, false);
            }
        }
    }
    tree()->redraw();
}

void presence_item_t::populate_dummy_child() {
    if (presence->get_entity()->get_children().size() > 0) {
        auto t = tree();
        add(prefs(), "[dummy]", new tree_item_t(supervisor, t, false));
        t->close(this, 0);
    }
}

auto presence_item_t::get_presence() -> presentation::presence_t & { return *presence; }

auto presence_item_t::get_device() const -> const model::device_t * { return presence->get_device(); }

int presence_item_t::get_position(const presence_item_t &child, std::uint32_t cut_mask) {
    auto &container = presence->get_children();
    int position = 0;
    for (auto p : container) {
        if (p == child.presence) {
            break;
        }
        if (!is_hidden(p, cut_mask)) {
            ++position;
        }
    }
    return position;
}

presence_item_ptr_t presence_item_t::do_hide() {
    auto host = parent();
    auto r = presence_item_ptr_t(this);
    if (host) {
        auto index = host->find_child(this);
        if (index >= 0) {
            if (is_selected()) {
                select_other();
            }
            if ((presence->get_features() & F::folder)) {
                host->deparent(index);
            } else {
                dynamic_cast<presence_item_t *>(host)->safe_detach(index);
            }
            host->tree()->redraw();
        }
    }
    return r;
}

bool presence_item_t::show(std::uint32_t hide_mask, bool refresh_labels, int32_t depth) {
    using nodes_storage_t = std::unordered_map<presentation::entity_t *, presence_item_ptr_t>;
    assert(depth >= 0);
    if (is_hidden(presence, hide_mask)) {
        do_hide();
        return false;
    }
    if (refresh_labels) {
        update_label();
    }
    auto nodes = nodes_storage_t();
    if (depth >= 1 && expanded) {
        while (children()) {
            auto node = safe_detach(0);
            nodes.emplace(node->get_presence().get_entity(), std::move(node));
        }
        auto get_or_create_child = [&](presence_t *presence) -> presence_item_ptr_t {
            auto it = nodes.find(presence->get_entity());
            if (it != nodes.end() && it->second->presence == presence) {
                auto node = std::move(it->second);
                nodes.erase(it);
                return node;
            }
            return make_item(this, *presence);
        };
        auto &children = presence->get_children();
        for (int i = 0, j = 0; i < (int)children.size(); ++i) {
            auto c = children[i];
            if (!is_hidden(c, hide_mask)) {
                auto item = get_or_create_child(c).detach();
                reparent(item, j++);
                item->show(hide_mask, refresh_labels, depth - 1);
            }
        }
    }
    tree()->redraw();
    return true;
}

void presence_item_t::show_child(presentation::presence_t &child_presence, std::uint32_t mask) {
    auto aug = child_presence.get_augmentation().get();
    if (is_expanded()) {
        bool found = false;
        for (int i = 0; i < this->children(); ++i) {
            auto c = static_cast<presence_item_t *>(child(i));
            auto &p = c->get_presence();
            if (p.get_entity() == child_presence.get_entity()) {
                if (&p != &child_presence) {
                    auto child_holder = presence_item_ptr_t();
                    bool need_open = c->is_expanded();
                    if (p.get_features() & F::missing) {
                        child_holder = safe_detach(i);
                    }
                    auto node = make_item(this, child_presence);
                    for (int j = 0; child_holder && child_holder->children(); ++j) {
                        auto n = child_holder->deparent(0);
                        node->reparent(n, j);
                    }
                    insert_node(node, i);
                    node->show(mask, true, 0);
                    if (need_open) {
                        node->open();
                        node->expanded = true;
                    }
                }
                found = true;
                break;
            }
        }
        if (!found) {
            show(mask, true, 1);
        }
    } else if (children() == 0) {
        populate_dummy_child();
    }
}

void presence_item_t::update_label() {
    using allocator_t = std::pmr::polymorphic_allocator<char>;
    using string_t = std::basic_string<char, std::char_traits<char>, allocator_t>;
    auto buffer = std::array<std::byte, 256>();
    auto pool = std::pmr::monotonic_buffer_resource(buffer.data(), buffer.size());
    auto allocator = allocator_t(&pool);

    auto color = get_color();
    auto name = presence->get_entity()->get_path().get_own_name();
    auto features = presence->get_features();
    auto node_label = string_t(allocator);
    if (features & F::folder) {
        auto folder_presence = static_cast<presentation::folder_presence_t *>(presence);
        auto folder = folder_presence->get_folder_info().get_folder();
        auto folder_label = folder->get_label();
        auto folder_id = folder->get_id();
        fmt::format_to(std::back_inserter(node_label), "{}, {}", folder_label, folder_id);
    } else {
        node_label = string_t(name, allocator);
    }

    if (features & (F::directory | F::folder)) {
        auto &ps = presence->get_stats(true);
        auto &es = presence->get_entity()->get_stats();
        if (ps.size && (ps.cluster_entries != es.entities)) {
            double share = (100.0 * ps.cluster_entries) / es.entities;
            fmt::format_to(std::back_inserter(node_label), " ({}{:.2f}%)", symbols::synchronizing, share);
        }
        if (features & F::local && (ps.local_entries != ps.entities)) {
            double share = (100.0 * ps.local_entries) / es.entities;
            fmt::format_to(std::back_inserter(node_label), " ({}{:.2f}%)", symbols::scanning, share);
        }
    }

    labelfgcolor(color);
    label(node_label.data());
}

Fl_Color presence_item_t::get_color() const {
    auto &cfg = supervisor.get_app_config().fltk_config;
    if (cfg.display_colorized) {
        auto f = presence->get_features();
        if (f & F::missing) {
            return FL_DARK_RED;
        } else if (f & F::deleted) {
            return FL_DARK1;
        } else if (f & F::symblink) {
            return FL_DARK_BLUE;
        } else if (f & F::conflict) {
            return FL_RED;
        }
        auto &ps = presence->get_stats(true);
        auto &s = presence->get_entity()->get_stats();
        auto in_sync = ps.cluster_entries == s.entities;
        if (in_sync) {
            return FL_DARK_GREEN;
        }
    }
    return FL_BLACK;
}

presence_item_ptr_t presence_item_t::safe_detach(int child_index) {
    auto item = presence_item_ptr_t();
    auto child = deparent(child_index);
    if (child) {
        auto presence_item = dynamic_cast<presence_item_t *>(child);
        if (!presence_item) {
            delete child;
        }
        item = presence_item_ptr_t(presence_item, false);
    }
    return item;
}

void presence_item_t::insert_node(presence_item_ptr_t node, int position) {
    auto tmp_node = insert(prefs(), "", position);
    auto ptr = node.detach();
    auto p = ptr->presence;
    if (p->get_features() & F::directory && ptr->children() == 0) {
        if (p->get_children().size() > 0) {
            ptr->populate_dummy_child();
        }
    }
    replace_child(tmp_node, ptr);
}

bool presence_item_t::is_expanded() const { return expanded; }


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

#pragma once

#include "tree_item.h"
#include "presentation/presence.h"
#include "presentation/entity.h"

namespace syncspirit::fltk {

struct presence_item_t;

using presence_item_ptr_t = model::intrusive_ptr_t<presence_item_t>;

struct presence_item_t : dynamic_item_t, model::augmentation_t {
    using parent_t = dynamic_item_t;
    using parent_t::on_update;

    presence_item_t(presentation::presence_t &presence, app_supervisor_t &supervisor, Fl_Tree *tree,
                    bool augment = true);
    ~presence_item_t();

    bool show(std::uint32_t hide_mask, bool refresh_labels, std::int32_t depth) override;
    bool is_expanded() const;
    void on_open() override;
    presentation::presence_t &get_presence();
    void show_child(presentation::presence_t &child_presence, std::uint32_t mask);
    void update_label() override;
    presence_item_ptr_t safe_detach(int child_index);

  protected:
    void insert_node(presence_item_ptr_t node, int position);
    void populate_dummy_child();
    Fl_Color get_color() const;
    void do_show(std::uint32_t mask, bool refresh_label);
    presence_item_ptr_t do_hide();
    int get_position(const presence_item_t &child, std::uint32_t cut_mask);
    virtual const model::device_t *get_device() const;

    presentation::presence_t *presence;
    bool expanded;
};

} // namespace syncspirit::fltk


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

#include "cluster.h"

using namespace syncspirit::fltk;
using namespace syncspirit::fltk::tree_item;

using F = syncspirit::presentation::presence_t::features_t;

cluster_t::cluster_t(presentation::presence_t &presence_, app_supervisor_t &supervisor, Fl_Tree *tree)
    : parent_t(presence_, supervisor, tree) {
    auto f = presence_.get_features();
    assert(f & (F::local | F::cluster));
    update_label();
}


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

#pragma once

#include "file.h"

namespace syncspirit::fltk::tree_item {

struct cluster_t final : file_t {
    using parent_t = file_t;

    cluster_t(presentation::presence_t &presence, app_supervisor_t &supervisor, Fl_Tree *tree);
};

} // namespace syncspirit::fltk::tree_item


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

#include "file.h"
#include "content/file_table.h"

using namespace syncspirit::fltk;
using namespace syncspirit::fltk::tree_item;

using F = syncspirit::presentation::presence_t::features_t;

bool file_t::on_select() {
    content = supervisor.replace_content([&](content_t *content) -> content_t * {
        auto prev = content->get_widget();
        int x = prev->x(), y = prev->y(), w = prev->w(), h = prev->h();
        return new content::file_table_t(*this, x, y, w, h);
    });
    return true;
}


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

#pragma once

#include "presence_item.h"

namespace syncspirit::fltk::tree_item {

struct file_t : presence_item_t {
    using parent_t = presence_item_t;
    using parent_t::parent_t;

    bool on_select() override;
};

} // namespace syncspirit::fltk::tree_item


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

#include "folder.h"
#include "symbols.h"
#include "table_widget/checkbox.h"
#include "table_widget/choice.h"
#include "table_widget/input.h"
#include "table_widget/label.h"
#include "content/folder_table.h"
#include "utils.hpp"
#include "presentation/folder_presence.h"
#include <boost/smart_ptr/local_shared_ptr.hpp>
#include <spdlog/fmt/fmt.h>

#if defined(WIN32) || defined(_WIN32) || defined(__WIN32)
#include <malloc.h>
#else
#include <alloca.h>
#endif

using namespace syncspirit;
using namespace model::diff;
using namespace syncspirit::fltk;
using namespace syncspirit::fltk::presence_item;

static constexpr int padding = 2;

namespace {

using folder_table_t = content::folder_table_t;

static auto make_actions(folder_table_t &container) -> widgetable_ptr_t {
    struct widget_t final : widgetable_t {
        using parent_t = widgetable_t;
        using parent_t::parent_t;

        Fl_Widget *create_widget(int x, int y, int w, int h) override {
            auto group = new Fl_Group(x, y, w, h);
            group->begin();
            group->box(FL_FLAT_BOX);
            auto &container = static_cast<folder_table_t &>(this->container);

            auto yy = y + padding, ww = 100, hh = h - padding * 2;

            auto apply = new Fl_Button(x + padding, yy, ww, hh, "apply");
            apply->deactivate();
            apply->callback([](auto, void *data) { static_cast<folder_table_t *>(data)->on_apply(); }, &container);
            container.apply_button = apply;
            int xx = apply->x() + ww + padding * 2;

            auto reset = new Fl_Button(xx, yy, ww, hh, "reset");
            reset->deactivate();
            reset->callback([](auto, void *data) { static_cast<folder_table_t *>(data)->on_reset(); }, &container);
            container.reset_button = reset;
            xx = reset->x() + ww + padding * 2;

            auto rescan = new Fl_Button(xx, yy, ww, hh, "rescan");
            rescan->callback([](auto, void *data) { static_cast<folder_table_t *>(data)->on_rescan(); }, &container);
            rescan->deactivate();
            container.rescan_button = rescan;
            xx = rescan->x() + ww + padding * 2;

            auto remove = new Fl_Button(xx, yy, ww, hh, "remove");
            remove->callback([](auto, void *data) { static_cast<folder_table_t *>(data)->on_remove(); }, &container);
            remove->color(FL_RED);
            xx = remove->x() + ww + padding * 2;

            group->resizable(nullptr);
            group->end();
            widget = group;

            this->reset();
            return widget;
        }
    };

    return new widget_t(container);
}

struct table_t : content::folder_table_t {
    using parent_t = content::folder_table_t;
    using parent_t::parent_t;

    table_t(tree_item_t &container_, const model::folder_info_t &description, int x, int y, int w, int h)
        : parent_t(container_, description, x, y, w, h) {

        auto &folder_item = static_cast<folder_t &>(container);
        auto &fp = static_cast<presentation::folder_presence_t &>(folder_item.get_presence());
        is_local = fp.get_features() & presentation::presence_t::features_t::local;

        entries_cell = new static_string_provider_t();
        entries_size_cell = new static_string_provider_t();
        max_sequence_cell = new static_string_provider_t();
        scan_start_cell = new static_string_provider_t();
        scan_finish_cell = new static_string_provider_t();

        auto data = table_rows_t();
        data.push_back({"", make_title(*this, "edit existing folder")});
        data.push_back({"path", make_path(*this, true)});
        data.push_back({"id", make_id(*this, true)});
        data.push_back({"label", make_label(*this, !is_local)});
        if (is_local) {
            data.push_back({"type", make_folder_type(*this, false)});
            data.push_back({"pull order", make_pull_order(*this, false)});
        }
        data.push_back({"cluster/local entries", entries_cell});
        data.push_back({"entries size", entries_size_cell});
        data.push_back({"max sequence", max_sequence_cell});
        data.push_back({"index", make_index(*this, true)});
        if (is_local) {
            data.push_back({"scan start", scan_start_cell});
            data.push_back({"scan finish", scan_finish_cell});
            data.push_back({"rescan interval", make_rescan_interval(*this, false)});
            data.push_back({"ignore permissions", make_ignore_permissions(*this, false)});
            data.push_back({"ignore delete", make_ignore_delete(*this, false)});
            data.push_back({"disable temp indixes", make_disable_tmp(*this, false)});
            data.push_back({"scheduled", make_scheduled(*this, false)});
            data.push_back({"paused", make_paused(*this, false)});
        }

        if (is_local) {
            int shared_count = 0;
            for (auto it : *shared_with) {
                auto &device = it.item;
                auto widget = make_shared_with(*this, device, false);
                data.push_back({"shared_with", widget});
                ++shared_count;
            }
            if (!shared_count) {
                auto widget = make_shared_with(*this, {}, false);
                data.push_back({"shared_with", widget});
            }
            data.push_back({"", notice = make_notice(*this)});
            data.push_back({"actions", make_actions(*this)});

            initially_shared_with = *shared_with;
            initially_non_shared_with = *non_shared_with;
        }

        assign_rows(std::move(data));
        refresh();
    }

    void refresh() override {
        auto &folder_item = static_cast<folder_t &>(container);
        auto &fp = static_cast<presentation::folder_presence_t &>(folder_item.get_presence());
        auto is_local = fp.get_features() & presentation::presence_t::features_t::local;

        if (is_local) {
            auto &folder_info = fp.get_folder_info();
            serialization_context_t ctx;
            description.get_folder()->serialize(ctx.folder);

            auto copy_data = db::encode(ctx.folder);
            error = {};
            auto valid = store(&ctx);

            // clang-format off
            auto is_same = (copy_data == db::encode(ctx.folder))
                        && (initially_shared_with == ctx.shared_with);
            // clang-format on
            if (!is_same) {
                if (valid) {
                    apply_button->activate();
                }
                reset_button->activate();
                rescan_button->deactivate();
            } else {
                apply_button->deactivate();
                auto &folders = container.supervisor.get_cluster()->get_folders();
                auto folder_id = db::get_id(ctx.folder);
                auto folder = folders.by_id(folder_id);
                if (!folder->is_scanning()) {
                    rescan_button->activate();
                }
                reset_button->deactivate();
            }

            auto folder = description.get_folder();
            auto &date_start = folder->get_scan_start();
            auto &date_finish = folder->get_scan_finish();
            auto scan_start = date_start.is_not_a_date_time() ? "-" : model::pt::to_simple_string(date_start);
            auto scan_finish = date_finish.is_not_a_date_time() ? "-" : model::pt::to_simple_string(date_finish);
            scan_start_cell->update(scan_start);
            scan_finish_cell->update(scan_finish);
        }

        auto max_sequence = description.get_max_sequence();
        auto &stats = fp.get_stats();
        entries_cell->update(fmt::format("{}", stats.entities));
        entries_size_cell->update(get_file_size(stats.size));
        max_sequence_cell->update(fmt::format("{}", max_sequence));

        if (is_local) {
            notice->reset();
        }
        redraw();
    }

    bool is_local;
};

} // namespace

folder_t::folder_t(presentation::folder_presence_t &presence, app_supervisor_t &supervisor, Fl_Tree *tree)
    : parent_t(presence, supervisor, tree) {
    auto cluster = supervisor.get_cluster();
    update_label();
    populate_dummy_child();
}

folder_t::~folder_t() {
    if (auto p = parent(); p) {
        auto index = p->find_child(this);
        if (index >= 0) {
            if (is_selected()) {
                select_other();
            }
            p->deparent(index);
            static_cast<tree_item_t *>(p)->update_label();
        }
    }
}

bool folder_t::on_select() {
    content = supervisor.replace_content([&](content_t *content) -> content_t * {
        using devices_ptr_t = table_t::shared_devices_t;
        auto prev = content->get_widget();
        auto shared_with = devices_ptr_t(new model::devices_map_t{});
        auto non_shared_with = devices_ptr_t(new model::devices_map_t{});

        auto fp = static_cast<presentation::folder_presence_t *>(presence);
        auto &folder_info = fp->get_folder_info();

        int x = prev->x(), y = prev->y(), w = prev->w(), h = prev->h();
        return new table_t(*this, folder_info, x, y, w, h);
    });
    return true;
}


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

#pragma once

#include "presence_item.h"
#include "presentation/folder_entity.h"

namespace syncspirit::fltk::presence_item {

struct folder_t final : presence_item_t {
    using parent_t = presence_item_t;

    folder_t(presentation::folder_presence_t &folder, app_supervisor_t &supervisor, Fl_Tree *tree);
    ~folder_t();
    bool on_select() override;
};

} // namespace syncspirit::fltk::presence_item


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

#include "missing.h"
#include "presentation/missing_file_presence.h"

using namespace syncspirit::fltk;
using namespace syncspirit::fltk::tree_item;

missing_t::missing_t(presence_item_t *host_, presentation::presence_t &presence_)
    : parent_t(presence_, host_->supervisor, host_->tree(), false), host{host_} {
    using F = presentation::presence_t::features_t;
    assert(presence_.get_features() & F::missing);
    update_label();
    if (presence_.get_entity()->get_children().size()) {
        populate_dummy_child();
    }
    auto &p = static_cast<presentation::missing_file_presence_t &>(presence_);
    p.add(this);
}

missing_t::~missing_t() {
    auto p = static_cast<presentation::missing_file_presence_t *>(presence);
    presence = {};
    p->remove(this);
}

auto missing_t::get_device() const -> const model::device_t * { return host->get_presence().get_device(); }

void missing_t::on_delete() noexcept {
    select_other();
    if (auto p = parent(); p) {
        if (auto index = p->find_child(this); index >= 0) {
            auto holder = dynamic_cast<presence_item_t *>(p)->safe_detach(index);
            if (!presence) { // it is already in d-tor
                holder.detach();
            }
        }
    }
}


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

#pragma once

#include "file.h"

namespace syncspirit::fltk::tree_item {

struct missing_t final : file_t {
    using parent_t = file_t;

    missing_t(presence_item_t *host, presentation::presence_t &presence);
    ~missing_t();

    const model::device_t *get_device() const override;
    void on_delete() noexcept override;

    presence_item_t *host;
};

} // namespace syncspirit::fltk::tree_item


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

#include "qr_button.h"

#include <memory>
#include <system_error>
#include <algorithm>
#include <vector>

#include <qrencode.h>
#include <boost/dynamic_bitset.hpp>

#include <FL/Fl.H>
#include <FL/Fl_Group.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Bitmap.H>

namespace syncspirit::fltk {

namespace {

static constexpr int PADDING = 10;

template <typename T> using guard_t = std::unique_ptr<T, std::function<void(T *)>>;

template <typename T, typename G> guard_t<T> make_guard(T *ptr, G &&fn) {
    return guard_t<T>{ptr, [fn = std::move(fn)](T *it) { fn(it); }};
}

using code_t = guard_t<QRcode>;

struct button_t : Fl_Button {
    using image_t = std::unique_ptr<Fl_Image>;
    using bits_t = std::vector<unsigned char>;
    using bit_set_t = boost::dynamic_bitset<unsigned char>;
    using parent_t = Fl_Button;

    button_t(app_supervisor_t &sup_, code_t code_, int x, int y, int w, int h, const char *label_,
             std::string_view short_id_)
        : parent_t(x, y, w, h), sup{sup_}, code{std::move(code_)}, scale{0}, data{label_}, short_id(short_id_) {
        regen_image(w, h);
        tooltip("copy device id to clipboard");

        int xx, yy, ww, hh;
        recalc_dimensions(x, y, w, h, xx, yy, ww, hh);
        this->x(xx);
        this->y(yy);
        this->w(ww);
        this->h(hh);

        callback(
            [](Fl_Widget *widget, void *data) {
                auto button = static_cast<button_t *>(widget);
                auto device_id = button->data;
                Fl::copy(device_id, strlen(device_id), 1);
                auto sup = reinterpret_cast<app_supervisor_t *>(data);
                sup->get_logger()->info("device id {} has been copied to clipboard", button->short_id);
            },
            &sup);
    }

    void regen_image(int w, int h) {
        auto min_w = std::min(w - PADDING * 2, h - PADDING * 2);
        min_w = std::max(code->width, min_w);
        auto new_scale = min_w / code->width;
        if (new_scale == scale) {
            return;
        }

        auto img_w = code->width * new_scale;
        auto extra_w = img_w % 8;
        auto line_w = img_w + (extra_w ? (8 - extra_w) : 0);
        bit_set_t bit_set(std::size_t(img_w * line_w));
        // logger->debug("qr code v{}, width = {}, scale = {}", code->version, code->width, scale);
        for (int i = 0; i < code->width; ++i) {
            for (int j = 0; j < code->width; ++j) {
                auto bit = code->data[i * code->width + j] & 1;
                for (int y = i * new_scale; y < (i + 1) * new_scale; ++y) {
                    for (int x = j * new_scale; x < (j + 1) * new_scale; ++x) {
                        bit_set.set(y * line_w + x, bit);
                    }
                }
            }
        }

        bits.clear();
        bits.reserve(bit_set.num_blocks());
        boost::to_block_range(bit_set, std::back_insert_iterator(bits));
        qr_image.reset(new Fl_Bitmap(bits.data(), img_w, img_w));
        image(qr_image.get());
        scale = new_scale;
    }

    void recalc_dimensions(int x, int y, int w, int h, int &xx, int &yy, int &ww, int &hh) {
        auto img = image();
        ww = img->w() + PADDING * 2;
        hh = img->h() + PADDING * 2;
        xx = x + w / 2 - ww / 2;
        yy = y + h / 2 - hh / 2;
    }

    void resize(int, int, int, int) override {
        int x = parent()->x();
        int y = parent()->y();
        int w = parent()->w();
        int h = parent()->h();
        regen_image(w, h);
        int xx, yy, ww, hh;
        recalc_dimensions(x, y, w, h, xx, yy, ww, hh);
        parent_t::resize(xx, yy, ww, hh);
    }

    app_supervisor_t ⊃
    code_t code;
    bits_t bits;
    image_t qr_image;
    int scale = 0;
    const char *data;
    std::string short_id;
};

} // namespace

qr_button_t::qr_button_t(const model::device_id_t &device_, app_supervisor_t &supervisor_, int x, int y, int w, int h)
    : parent_t(x, y, w, h), device_id{device_}, supervisor{supervisor_} {

    box(FL_FLAT_BOX);

    auto device_id_raw = device_id.get_value().c_str();
    auto device_id_short = device_id.get_short();
    auto code_raw = QRcode_encodeString(device_id_raw, 0, QR_ECLEVEL_H, QR_MODE_8, 1);
    auto &logger = supervisor.get_logger();
    if (!code_raw) {
        auto ec = std::error_code{errno, std::generic_category()};
        logger->error("cannot generate qr code ({}) : {}", ec.value(), ec.message());
        return;
    }
    auto code = make_guard(code_raw, [](auto ptr) { QRcode_free(ptr); });
    auto button = new button_t(supervisor, std::move(code), x, y, w, h, device_id_raw, device_id_short);
    resizable(button);
}

} // namespace syncspirit::fltk


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

#pragma once

#include "app_supervisor.h"

#include <FL/Fl_Group.H>

namespace syncspirit::fltk {

struct qr_button_t : Fl_Group {
    using parent_t = Fl_Group;

    qr_button_t(const model::device_id_t &device, app_supervisor_t &supervisor, int x, int y, int w, int h);

    model::device_id_t device_id;
    app_supervisor_t &supervisor;
};

} // namespace syncspirit::fltk


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

#include "static_table.h"

#include "log_colors.h"
#include "log_utils.h"
#include <FL/fl_draw.H>
#include <algorithm>
#include <sstream>

using namespace syncspirit::fltk;

static constexpr int PADDING = 5;

widgetable_t::widgetable_t(Fl_Widget &container_) : container{container_}, widget{nullptr} {}

widgetable_t::~widgetable_t() {
    if (widget) {
        delete widget;
    }
}

Fl_Widget *widgetable_t::get_widget() { return widget; }

void widgetable_t::reset() {}

bool widgetable_t::store(void *) { return true; }

static void resize_value(const string_provider_ptr_t &, int, int, int, int) {}

static void resize_value(widgetable_ptr_t &widget, int x, int y, int w, int h) {
    auto impl = widget->get_widget();
    if (impl) {
        impl->resize(x, y, w, h);
        if (auto group = dynamic_cast<Fl_Group *>(impl); group) {
            group->init_sizes();
        }
        impl->redraw();
    };
}

static void reset_widget(string_provider_ptr_t &) {}
static void reset_widget(widgetable_ptr_t &widget) { widget->reset(); }

// static bool store_widget(string_provider_t *&, void *) { return true; }
static bool store_widget(widgetable_t *widget, void *data) { return widget->store(data); }

static_string_provider_t::static_string_provider_t(std::string_view value) : str{std::move(value)} {}
void static_string_provider_t::update(std::string value) { str = value; }
void static_string_provider_t::update(std::string_view value) { str = value; }
std::string_view static_string_provider_t::value() const { return str; }

static_table_t::static_table_t(int x, int y, int w, int h, table_rows_t &&rows_) : parent_t(x, y, w, h) {
    // box(FL_ENGRAVED_BOX);
    row_header(0);
    row_height_all(20);
    row_resize(0);
    cols(2);
    col_header(1);
    col_resize(1);
    end();
    when(FL_WHEN_CHANGED | when());
    resizable(this);

    set_visible_focus();
    resize(x, y, w, h);
    assign_rows(std::move(rows_));
}

void static_table_t::assign_rows(table_rows_t rows_) {
    table_rows = std::move(rows_);
    if (table_rows.size()) {
        rows(table_rows.size());
        create_widgets();
        reset();
        auto x = this->x(), y = this->y(), w = this->w(), h = this->h();
        resize(x, y, w, h);
        redraw();
    }
}

void static_table_t::draw_cell(TableContext context, int row, int col, int x, int y, int w, int h) {
    switch (context) {
    case CONTEXT_STARTPAGE: {
        fl_font(FL_HELVETICA, 16);
        auto col_widths = calc_col_widths();
        if (!col_widths.invalid) {
            auto col_min_size = std::min(col_widths.w1_min, col_widths.w2_min);
            if (col_width(0) < col_min_size || col_width(1) < col_min_size) {
                col_width(0, col_widths.w1);
                col_width(1, col_widths.w2);
            }
        }
        row_height_all(27);
        return;
    }
    case CONTEXT_COL_HEADER:
        fl_push_clip(x, y, w, h);
        fl_draw_box(FL_THIN_UP_BOX, x, y, w, h, row_header_color());
        fl_pop_clip();
        return;
    case CONTEXT_CELL:
        draw_data(row, col, x, y, w, h);
        return;
    case CONTEXT_RC_RESIZE: {
        auto w0 = col_width(0);
        auto col_widths = calc_col_widths();
        auto col_min_size = std::min(col_widths.w1_min, col_widths.w2_min);
        if (w0 < col_min_size) {
            col_width(0, col_min_size);
            w0 = col_min_size;
        }
        auto last_sz = this->tiw - w0;
        if (last_sz >= col_min_size) {
            col_width(1, last_sz);
        }
        resize_widgets();
        return;
    }
    default:
        return;
    }
}

void static_table_t::resize(int x, int y, int w, int h) {
    parent_t::resize(x, y, w, h);
    auto col_widths = calc_col_widths();
    col_width(0, col_widths.w1);
    col_width(1, col_widths.w2);
    init_sizes();
}

auto static_table_t::calc_col_widths() -> col_sizes_t {
    fl_font(FL_HELVETICA, 16);
    auto w = tiw;
    if (table_rows.empty()) {
        auto w2 = w / 2;
        return {w2, w2, w2, w2, true};
    }

    col_sizes_t r = {0, 0, 0, 0, true};
    for (auto &row : table_rows) {
        int x, y, w, h;
        fl_text_extents(row.label.data(), x, y, w, h);
        r.w1_min = std::max(r.w1_min, w + PADDING * 2);
        std::visit([&](auto &&arg) { calc_dimensions(arg, x, y, w, h); }, row.value);
        r.w2_min = std::max(r.w2_min, w + PADDING * 2);
    }
    r.w1 = r.w1_min;
    r.w2 = r.w2_min;
    auto delta = w - (r.w1 + r.w2);
    if (r.w1 + r.w2 < w) {
        auto tmp_w1_1 = r.w1 + static_cast<int>(delta * (double(r.w1_min) / r.w2_min));
        auto tmp_w1_2 = r.w1_min * 3 / 2;
        r.w1 = std::min(tmp_w1_1, tmp_w1_2);
        r.w2 = w - r.w1;
    }
    r.invalid = r.w1_min == PADDING * 2;
    return r;
}

void static_table_t::draw_data(int r, int col, int x, int y, int w, int h) {
    auto &row = table_rows.at(static_cast<size_t>(r));
    if (col == 0) {
        draw_label(row.label, x, y, w, h);
    } else {
        std::visit([&](auto &&arg) { draw_value(arg, x, y, w, h); }, row.value);
    }
}

auto static_table_t::get_rows() -> table_rows_t & { return table_rows; }

void static_table_t::draw_text(const std::string_view &text, int x, int y, int w, int h) {
    fl_push_clip(x, y, w, h);
    {
        fl_font(FL_HELVETICA, 16);
        Fl_Align align = FL_ALIGN_LEFT;
        int dx = PADDING;
        int dw = 0;

        fl_color(FL_WHITE);
        fl_rectf(x, y, w, h);
        fl_color(FL_GRAY0);
        fl_draw(text.data(), x + dx, y, w + dw, h, align);
        fl_color(color());
        fl_rect(x, y, w, h);
    }
    fl_pop_clip();
}

void static_table_t::calc_dimensions(const string_provider_ptr_t &provider, int &x, int &y, int &w, int &h) {
    auto str = provider->value();
    fl_text_extents(str.data(), x, y, w, h);
}

void static_table_t::calc_dimensions(const widgetable_ptr_t &, int &, int &, int &, int &) {}

void static_table_t::draw_label(const std::string &value, int x, int y, int w, int h) { draw_text(value, x, y, w, h); }

void static_table_t::draw_value(const string_provider_ptr_t &provider, int x, int y, int w, int h) {
    auto value = provider->value();
    draw_text(value, x, y, w, h);
}

void static_table_t::draw_value(const widgetable_ptr_t &, int, int, int, int) {}

void static_table_t::create_widgets() {
    for (int i = 0; i < static_cast<int>(table_rows.size()); ++i) {
        auto &row = table_rows[i];
        std::visit([&](auto &&arg) { make_widget(arg, i); }, row.value);
    }
}

void static_table_t::make_widget(const string_provider_ptr_t &, int) {}

void static_table_t::make_widget(const widgetable_ptr_t &w, int row) {
    int xx, yy, ww, hh;
    find_cell(CONTEXT_TABLE, row, 1, xx, yy, ww, hh);
    auto backend = w->create_widget(xx, yy, ww, hh);
    add(backend);
}

void static_table_t::reset() {
    for (int i = 0; i < static_cast<int>(table_rows.size()); ++i) {
        auto &row = table_rows[i];
        std::visit([&](auto &arg) { reset_widget(arg); }, row.value);
    }
}

bool static_table_t::store(void *data) {
    bool ok = true;
    for (int i = 0; ok && i < static_cast<int>(table_rows.size()); ++i) {
        auto &value = table_rows[i].value;
        // unknown crash on win32/release if for string_provider, std::visit
        // is not used
        auto widget = std::get_if<widgetable_ptr_t>(&value);
        if (widget) {
            ok = store_widget(widget->get(), data);
        }
    }
    return ok;
}

int static_table_t::find_row(const widgetable_t &item) {
    for (int i = 0; i < static_cast<int>(table_rows.size()); ++i) {
        auto &row = table_rows[i];
        auto widget = std::get_if<widgetable_ptr_t>(&row.value);
        if (widget && widget->get() == &item) {
            return i;
        }
    }
    return -1;
}

void static_table_t::resize_widgets() {
    for (int i = 0; i < rows(); ++i) {
        auto &row = table_rows.at(static_cast<size_t>(i));
        std::visit(
            [&](auto &&arg) {
                int xx, yy, ww, hh;
                find_cell(CONTEXT_TABLE, i, 1, xx, yy, ww, hh);
                resize_value(arg, xx, yy, ww, hh);
            },
            row.value);
    }
    init_sizes();
}

void static_table_t::remove_row(int index) {
    if (index >= 0) {
        if (index != static_cast<int>(table_rows.size()) - 1) {
            table_rows[index].value = {};
            for (int j = index + 1; j < static_cast<int>(table_rows.size()); ++j) {
                table_rows[j - 1] = std::move(table_rows[j]);
            }
        }
        table_rows.resize(table_rows.size() - 1);
        rows(table_rows.size());
        resize_widgets();
    }
}

void static_table_t::remove_row(widgetable_t &item) { remove_row(find_row(item)); }

void static_table_t::insert_row(std::string_view label, value_provider_t w, size_t index) {
    assert(index <= table_rows.size());
    table_rows.resize(table_rows.size() + 1);
    for (size_t j = table_rows.size() - 1; j > index; --j) {
        table_rows[j] = std::move(table_rows[j - 1]);
    }
    table_rows[index] = table_row_t{label, w};
    rows(table_rows.size());
    std::visit([&](auto &&arg) { make_widget(arg, static_cast<int>(index)); }, w);
    resize_widgets();
    redraw();
}


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

#pragma once

#include "content.h"
#include <FL/Fl_Table_Row.H>

#include <boost/intrusive_ptr.hpp>
#include <boost/smart_ptr/intrusive_ref_counter.hpp>

#include <string>
#include <vector>
#include <utility>
#include <variant>

namespace syncspirit::fltk {

struct widgetable_t : boost::intrusive_ref_counter<widgetable_t, boost::thread_unsafe_counter> {
    widgetable_t(Fl_Widget &container);
    virtual ~widgetable_t();

    virtual Fl_Widget *create_widget(int x, int y, int w, int h) = 0;
    Fl_Widget *get_widget();
    virtual void reset();
    virtual bool store(void *);

    Fl_Widget &container;
    Fl_Widget *widget = nullptr;
};

using widgetable_ptr_t = boost::intrusive_ptr<widgetable_t>;

struct string_provider_t : boost::intrusive_ref_counter<string_provider_t, boost::thread_unsafe_counter> {
    string_provider_t() = default;
    virtual ~string_provider_t() = default;

    virtual std::string_view value() const = 0;
};
using string_provider_ptr_t = boost::intrusive_ptr<string_provider_t>;

struct static_string_provider_t : string_provider_t {
    static_string_provider_t(std::string_view value = {});
    void update(std::string value);
    void update(std::string_view value);

    std::string_view value() const override;

  private:
    std::string str;
};
using static_string_provider_ptr_t = boost::intrusive_ptr<static_string_provider_t>;

using value_provider_t = std::variant<string_provider_ptr_t, widgetable_ptr_t>;

struct table_row_t {
    std::string label;
    value_provider_t value;

    table_row_t() = default;

    template <typename L, typename V>
    table_row_t(L &&label_, V &&value_) : label{std::forward<L>(label_)}, value{std::forward<V>(value_)} {}
};

using table_rows_t = std::vector<table_row_t>;

struct static_table_t : contentable_t<Fl_Table> {
    using parent_t = contentable_t<Fl_Table>;

    struct col_sizes_t {
        int w1;
        int w2;
        int w1_min;
        int w2_min;
        bool invalid;
    };

    static_table_t(int x, int y, int w, int h, table_rows_t &&rows = {});

    void assign_rows(table_rows_t rows);
    void draw_cell(TableContext context, int row, int col, int x, int y, int w, int h) override;
    void resize(int x, int y, int w, int h) override;
    table_rows_t &get_rows();
    void remove_row(widgetable_t &);
    void remove_row(int index);
    int find_row(const widgetable_t &);
    void insert_row(std::string_view label, value_provider_t, size_t index);

    void reset() override;
    bool store(void *) override;

  private:
    void create_widgets();
    void draw_data(int row, int col, int x, int y, int w, int h);
    void draw_label(const std::string &, int x, int y, int w, int h);
    void draw_value(const string_provider_ptr_t &, int x, int y, int w, int h);
    void draw_value(const widgetable_ptr_t &, int x, int y, int w, int h);
    void calc_dimensions(const string_provider_ptr_t &, int &x, int &y, int &w, int &h);
    void calc_dimensions(const widgetable_ptr_t &, int &x, int &y, int &w, int &h);
    void draw_text(const std::string_view &, int x, int y, int w, int h);
    void make_widget(const string_provider_ptr_t &, int row);
    void make_widget(const widgetable_ptr_t &, int row);
    void resize_widgets();

    col_sizes_t calc_col_widths();

    table_rows_t table_rows;
};

} // namespace syncspirit::fltk


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

#include "symbols.h"

#define UTF8_CAST(X) std::string_view(reinterpret_cast<const char *>((X)))

namespace syncspirit::fltk::symbols {

static const auto scanning_raw = u8"∴";
static const auto synchronizing_raw = u8"↓";
static const auto online_raw = u8"↔";
static const auto offline_raw = u8"▽";
static const auto connecting_raw = u8"→";
static const auto discovering_raw = u8"…";
static const auto deleted_raw = u8"☠";
static const auto missing_raw = u8"⚙";
static const auto colorize_raw = u8"★";

// https://www.vertex42.com/ExcelTips/unicode-symbols.html

// ♨
// ♻
// ⚙
// ✆
// °
// …
// ≈
// ∞
// ∴
// ↑

const std::string_view scanning = UTF8_CAST(scanning_raw);
const std::string_view synchronizing = UTF8_CAST(synchronizing_raw);
const std::string_view online = UTF8_CAST(online_raw);
const std::string_view offline = UTF8_CAST(offline_raw);
const std::string_view connecting = UTF8_CAST(connecting_raw);
const std::string_view discovering = UTF8_CAST(discovering_raw);
const std::string_view deleted = UTF8_CAST(deleted_raw);
const std::string_view missing = UTF8_CAST(missing_raw);
const std::string_view colorize = UTF8_CAST(colorize_raw);

std::string_view get_description(std::string_view symbol) {
    if (symbol == scanning) {
        return "scanning";
    } else if (symbol == synchronizing) {
        return "synchronizing";
    } else if (symbol == online) {
        return "online";
    } else if (symbol == offline) {
        return "offline";
    } else if (symbol == connecting) {
        return "connecting";
    }
    return "?";
}

} // namespace syncspirit::fltk::symbols


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

#pragma once

#include <string_view>

namespace syncspirit::fltk::symbols {

extern const std::string_view online;
extern const std::string_view offline;
extern const std::string_view connecting;
extern const std::string_view discovering;
extern const std::string_view scanning;
extern const std::string_view synchronizing;
extern const std::string_view deleted;
extern const std::string_view missing;
extern const std::string_view colorize;

std::string_view get_description(std::string_view symbol);

} // namespace syncspirit::fltk::symbols


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

#include "checkbox.h"

static constexpr int padding = 2;

using namespace syncspirit::fltk::table_widget;

Fl_Widget *checkbox_t::create_widget(int x, int y, int w, int h) {
    auto group = new Fl_Group(x, y, w, h);
    group->begin();
    group->box(FL_FLAT_BOX);
    auto yy = y + padding, ww = 25, hh = h - padding * 2;
    input = new Fl_Check_Button(x + padding, yy, ww, hh);
    // auto xx = input->x() + ww + padding;

    group->end();
    widget = group;
    reset();
    return widget;
}


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

#include "../static_table.h"

#include <FL/Fl_Check_Button.H>

namespace syncspirit::fltk::table_widget {

struct checkbox_t : widgetable_t {
    using parent_t = widgetable_t;
    using parent_t::parent_t;

    Fl_Widget *create_widget(int x, int y, int w, int h) override;
    Fl_Check_Button *input = nullptr;
};

} // namespace syncspirit::fltk::table_widget


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

#include "choice.h"

static constexpr int padding = 2;

using namespace syncspirit::fltk::table_widget;

Fl_Widget *choice_t::create_widget(int x, int y, int w, int h) {
    auto group = new Fl_Group(x, y, w, h);
    group->begin();
    group->box(FL_FLAT_BOX);
    auto yy = y + padding, ww = w - padding * 2, hh = h - padding * 2;
    ww = std::min(150, ww);

    input = new Fl_Choice(x + padding, yy, ww, hh);
    group->end();
    group->resizable(nullptr);
    widget = group;

    reset();
    return widget;
}


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

#include "../static_table.h"

#include <FL/Fl_Choice.H>

namespace syncspirit::fltk::table_widget {

struct choice_t : widgetable_t {
    using parent_t = widgetable_t;
    using parent_t::parent_t;

    Fl_Widget *create_widget(int x, int y, int w, int h) override;
    Fl_Choice *input = nullptr;
};

} // namespace syncspirit::fltk::table_widget


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

#include "input.h"

static constexpr int padding = 2;

using namespace syncspirit::fltk::table_widget;

Fl_Widget *input_t::create_widget(int x, int y, int w, int h) {
    auto group = new Fl_Group(x, y, w, h);
    group->begin();
    group->box(FL_FLAT_BOX);
    auto yy = y + padding, ww = w - padding * 2, hh = h - padding * 2;

    input = new Fl_Input(x + padding, yy, ww, hh);
    group->end();
    widget = group;
    reset();
    return widget;
}


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

#include "../static_table.h"

#include <FL/Fl_Input.H>

namespace syncspirit::fltk::table_widget {

struct input_t : widgetable_t {
    using parent_t = widgetable_t;
    using parent_t::parent_t;

    Fl_Widget *create_widget(int x, int y, int w, int h) override;
    Fl_Input *input = nullptr;
};

} // namespace syncspirit::fltk::table_widget


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

#include "int_input.h"

static constexpr int padding = 2;

using namespace syncspirit::fltk::table_widget;

Fl_Widget *int_input_t::create_widget(int x, int y, int w, int h) {
    auto group = new Fl_Group(x, y, w, h);
    group->begin();
    group->box(FL_FLAT_BOX);
    auto yy = y + padding, ww = w - padding * 2, hh = h - padding * 2;

    input = new Fl_Int_Input(x + padding, yy, ww, hh);
    group->end();
    widget = group;
    reset();
    return widget;
}


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

#include "../static_table.h"

#include <FL/Fl_Int_Input.H>

namespace syncspirit::fltk::table_widget {

struct int_input_t : widgetable_t {
    using parent_t = widgetable_t;
    using parent_t::parent_t;

    Fl_Widget *create_widget(int x, int y, int w, int h) override;
    Fl_Int_Input *input = nullptr;
};

} // namespace syncspirit::fltk::table_widget


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

#include "label.h"

static constexpr int padding = 2;

using namespace syncspirit::fltk::table_widget;

Fl_Widget *label_t::create_widget(int x, int y, int w, int h) {
    input = new Fl_Box(x, y, w, h, "");
    input->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
    widget = input;
    reset();
    return widget;
}


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

#include "../static_table.h"

#include <FL/Fl_Box.H>

namespace syncspirit::fltk::table_widget {

struct label_t : widgetable_t {
    using parent_t = widgetable_t;
    using parent_t::parent_t;

    Fl_Widget *create_widget(int x, int y, int w, int h) override;
    Fl_Box *input = nullptr;
};

} // namespace syncspirit::fltk::table_widget


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

#include "path.h"

#include "FL/Fl_Button.H"

#if defined(WIN32) || defined(_WIN32) || defined(__WIN32)
#include <windows.h>
#endif

#include <FL/Fl_Native_File_Chooser.H>

static constexpr int padding = 2;

using namespace syncspirit::fltk::table_widget;

path_t::path_t(Fl_Widget &container_, std::string title_) : parent_t{container_}, title{title_} {}

Fl_Widget *path_t::create_widget(int x, int y, int w, int h) {
    auto group = new Fl_Group(x, y, w, h);
    group->begin();
    group->box(FL_FLAT_BOX);

    auto button_w = 25;
    auto yy = y + padding, ww = w - padding * 2, hh = h - padding * 2;
    input = new Fl_Input(x + padding, yy, ww - (button_w + padding * 2), hh);

    auto xx = input->x();
    auto button = new Fl_Button(xx + input->w() + padding, yy, button_w, hh, "...");
    button->callback([](Fl_Widget *, void *data) { reinterpret_cast<path_t *>(data)->on_click(); }, this);

    group->resizable(input);
    group->end();
    widget = group;
    reset();
    return widget;
}

void path_t::on_click() {
    using T = Fl_Native_File_Chooser::Type;
    auto type = T::BROWSE_DIRECTORY;

    Fl_Native_File_Chooser file_chooser;
    file_chooser.title(title.data());
    file_chooser.type(type);
    auto r = file_chooser.show();
    if (r != 0) {
        return;
    }

    input->value(file_chooser.filename());
}


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

#include "../static_table.h"

#include <FL/Fl_Input.H>
#include <string>

namespace syncspirit::fltk::table_widget {

struct path_t : widgetable_t {
    using parent_t = widgetable_t;

    path_t(Fl_Widget &container, std::string title);

    Fl_Widget *create_widget(int x, int y, int w, int h) override;
    void on_click();

    Fl_Input *input = nullptr;
    std::string title;
};

} // namespace syncspirit::fltk::table_widget


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

#include "toolbar.h"
#include "symbols.h"

#include <FL/Fl_Toggle_Button.H>

using namespace syncspirit::fltk;

static constexpr int padding = 5;
static constexpr int button_h = 20;

static void set_show_deleted(Fl_Widget *widget, void *data) {
    auto toolbar = reinterpret_cast<toolbar_t *>(data);
    auto button = static_cast<Fl_Toggle_Button *>(widget);
    auto value = button->value() ? true : false;
    toolbar->supervisor.set_show_deleted(value);
}

static void set_show_missing(Fl_Widget *widget, void *data) {
    auto toolbar = reinterpret_cast<toolbar_t *>(data);
    auto button = static_cast<Fl_Toggle_Button *>(widget);
    auto value = button->value() ? true : false;
    toolbar->supervisor.set_show_missing(value);
}

static void set_show_colorized(Fl_Widget *widget, void *data) {
    auto toolbar = reinterpret_cast<toolbar_t *>(data);
    auto button = static_cast<Fl_Toggle_Button *>(widget);
    auto value = button->value() ? true : false;
    toolbar->supervisor.set_show_colorized(value);
}

toolbar_t::toolbar_t(app_supervisor_t &supervisor_, int x, int y, int w, int)
    : parent_t(x, y, w, button_h + padding * 2), supervisor{supervisor_} {
    auto ww = 40 - padding * 2;
    auto xx = x + padding;
    auto yy = y + padding;

    [&]() {
        auto button = new Fl_Toggle_Button(xx, yy, ww, button_h, symbols::deleted.data());
        button->tooltip("show deleted");
        button->callback(set_show_deleted, this);
        bool value = supervisor.get_app_config().fltk_config.display_deleted;
        button->value(value ? 1 : 0);
        xx += ww + padding;
    }();

    [&]() {
        auto button = new Fl_Toggle_Button(xx, yy, ww, button_h, symbols::missing.data());
        button->tooltip("show missing");
        button->callback(set_show_missing, this);
        bool value = supervisor.get_app_config().fltk_config.display_missing;
        button->value(value ? 1 : 0);
        xx += ww + padding;
    }();

    [&]() {
        auto button = new Fl_Toggle_Button(xx, yy, ww, button_h, symbols::colorize.data());
        button->tooltip("show colorized");
        button->callback(set_show_colorized, this);
        bool value = supervisor.get_app_config().fltk_config.display_colorized;
        button->value(value ? 1 : 0);
    }();

    end();
    resizable(nullptr);
}


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

#pragma once

#include "app_supervisor.h"
#include <FL/Fl_Group.H>

namespace syncspirit::fltk {

struct toolbar_t : Fl_Group {
    using parent_t = Fl_Group;
    toolbar_t(app_supervisor_t &supervisor, int x, int y, int w, int h);

    app_supervisor_t &supervisor;
};

} // namespace syncspirit::fltk


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

#include "tree_item.h"
#include "utils.hpp"

using namespace syncspirit::fltk;

tree_item_t::tree_item_t(app_supervisor_t &supervisor_, Fl_Tree *tree, bool has_augmentation)
    : parent_t(tree), supervisor{supervisor_}, content{nullptr} {
    if (has_augmentation) {
        augmentation = new augmentation_t(this);
    }
}

tree_item_t::~tree_item_t() {
    if (augmentation) {
        augmentation->release_owner();
    }
}

bool tree_item_t::on_select() { return false; }

void tree_item_t::on_deselect() { content = nullptr; }

void tree_item_t::on_open() {}

void tree_item_t::on_close() {}

void tree_item_t::update_label() {}

void tree_item_t::refresh_content() {
    if (content) {
        supervisor.with_cluster([&]() { content->refresh(); });
    }
}

void tree_item_t::on_update() {
    supervisor.with_cluster([&]() {
        update_label();
        tree()->redraw();
        refresh_content();
    });
}

void tree_item_t::on_delete() {
    select_other();
    if (augmentation) {
        augmentation->release_owner();
    }
    auto upper = static_cast<tree_item_t *>(parent());
    if (upper) {
        upper->remove_child(this);
    }
}

void tree_item_t::select_other() {
    auto t = tree();
    supervisor.with_cluster([&]() {
        bool unselect = tree()->first_selected_item() == this;
        if (unselect) {
            Fl_Tree_Item *current = this;
            while (true) {
                auto prev = t->next_item(current, FL_Up, true);
                auto item = dynamic_cast<tree_item_t *>(prev);
                current = prev;
                if (item) {
                    if (item->on_select()) {
                        t->select(prev, 0);
                        t->redraw();
                        break;
                    }
                }
            }
        }
    });
}

auto tree_item_t::get_proxy() -> augmentation_ptr_t { return augmentation; }

auto tree_item_t::insert_by_label(tree_item_t *child_node, int start_index, int end_index) -> tree_item_t * {
    auto new_label = std::string_view(child_node->label());
    auto name_provider = [this](int index) { return std::string_view(child(index)->label()); };
    auto pos = bisect(new_label, start_index, end_index, children(), name_provider);
    auto tmp_node = insert(prefs(), "", pos);
    replace_child(tmp_node, child_node);
    return child_node;
}

void tree_item_t::remove_child(tree_item_t *child) {
    parent_t::remove_child(child);
    supervisor.with_cluster([&]() {
        update_label();
        tree()->redraw();
    });
}

void tree_item_t::apply(const node_visitor_t &visitor, void *data) {
    visitor.visit(*this, data);
    auto children_count = children();
    for (int i = 0; i < children_count; ++i) {
        auto node = static_cast<tree_item_t *>(child(i));
        node->apply(visitor, data);
    }
}


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

#pragma once

#include "app_supervisor.h"
#include "augmentation.h"
#include "content.h"

#include <FL/Fl_Tree_Item.H>
#include <FL/Fl_Tree.H>
#include <limits>

namespace syncspirit::fltk {

namespace outcome = model::outcome;
namespace diff = model::diff;

struct static_table_t;
struct node_visitor_t;

struct tree_item_t : Fl_Tree_Item {
    using parent_t = Fl_Tree_Item;
    static constexpr auto MAX_INDEX = std::numeric_limits<int>::max();

    tree_item_t(app_supervisor_t &supervisor, Fl_Tree *tree, bool has_augmentation = true);
    ~tree_item_t();

    virtual void update_label();
    void refresh_content();
    virtual void remove_child(tree_item_t *child);

    virtual bool on_select();
    virtual void on_deselect();
    virtual void on_update();
    virtual void on_delete();
    virtual void on_open();
    virtual void on_close();

    void select_other();
    augmentation_ptr_t get_proxy();

    template <typename Fn> auto within_tree(Fn &&fn) {
        using result_t = decltype(fn());
        if constexpr (std::is_same_v<result_t, void>) {
            auto t = tree();
            t->begin();
            fn();
            t->end();
            t->redraw();
            return;
        } else {
            auto t = tree();
            t->begin();
            auto &&r = fn();
            t->end();
            t->redraw();
            return std::move(r);
        }
    }

    void apply(const node_visitor_t &visitor, void *data);
    auto insert_by_label(tree_item_t *child, int start_index = 0, int end_index = MAX_INDEX) -> tree_item_t *;

    app_supervisor_t &supervisor;
    content_t *content;
    augmentation_ptr_t augmentation;
};

struct dynamic_item_t : tree_item_t {
    using parent_t = tree_item_t;
    using parent_t::parent_t;
    virtual bool show(std::uint32_t mask, bool refresh_labels, std::int32_t depth) = 0;
};

struct node_visitor_t {
    virtual ~node_visitor_t() = default;
    virtual void visit(tree_item_t &node, void *) const = 0;
};

} // namespace syncspirit::fltk


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

#include "devices.h"
#include "self_device.h"
#include "../table_widget/input.h"
#include "../table_widget/label.h"
#include "../static_table.h"
#include "model/device_id.h"
#include "model/diff/modify/update_peer.h"
#include "utils/format.hpp"

#include <FL/Fl_Scroll.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Input.H>
#include <FL/Fl_Box.H>

using namespace syncspirit;
using namespace syncspirit::model::diff;
using namespace syncspirit::fltk;
using namespace syncspirit::fltk::tree_item;

static constexpr int padding = 2;

namespace {

struct my_table_t;

struct serialization_context_t {
    std::string_view device_id;
    std::string_view label;
};

auto static make_device_id(my_table_t &container) -> widgetable_ptr_t;
auto static make_label(my_table_t &container) -> widgetable_ptr_t;
auto static make_notice(my_table_t &container) -> widgetable_ptr_t;
auto static make_actions(my_table_t &container) -> widgetable_ptr_t;

struct my_table_t : static_table_t {
    using parent_t = static_table_t;

    my_table_t(devices_t &container_, int x, int y, int w, int h)
        : parent_t(x, y, w, h), container{container_}, add_button{nullptr} {

        auto data = table_rows_t();

        data.push_back({"device_id", make_device_id(*this)});
        data.push_back({"label", make_label(*this)});
        data.push_back({"", notice = make_notice(*this)});
        data.push_back({"actions", make_actions(*this)});
        assign_rows(std::move(data));
    }

    void refresh() override {
        serialization_context_t ctx{};
        store(&ctx);

        auto device_opt = std::optional<model::device_id_t>{};

        error = "";
        if (!ctx.device_id.empty()) {
            device_opt = model::device_id_t::from_string(ctx.device_id);
            if (device_opt) {
                if (ctx.label.empty()) {
                    error = "no label given";
                }
            } else {
                error = "invalid device id";
            }
        }

        if (!ctx.device_id.empty() && error.empty()) {
            auto &peer = *device_opt;
            auto cluster = container.supervisor.get_cluster();
            auto &devices = cluster->get_devices();
            auto found = devices.by_sha256(peer.get_sha256());
            if (found) {
                error = "device already added";
            }
        }

        if (!error.empty() || ctx.device_id.empty()) {
            add_button->deactivate();
        } else {
            add_button->activate();
        }
        notice->reset();
    }

    void add_new_device() {
        serialization_context_t ctx{};
        store(&ctx);

        auto device_opt = model::device_id_t::from_string(ctx.device_id);
        auto &supervisor = container.supervisor;
        auto &log = supervisor.get_logger();
        if (!device_opt) {
            log->error("incorrect device_id");
            return;
        }

        auto &peer = *device_opt;
        auto &cluster = *supervisor.get_cluster();
        auto &devices = cluster.get_devices();
        auto found = devices.by_sha256(peer.get_sha256());
        if (found) {
            log->error("device {} is already added", peer);
            return;
        }

        db::Device db_dev;
        db::set_name(db_dev, ctx.label);

        auto diff = cluster_diff_ptr_t(new modify::update_peer_t(std::move(db_dev), peer, cluster));
        supervisor.send_model<model::payload::model_update_t>(std::move(diff), this);
    }

    widgetable_ptr_t notice;
    std::string_view error;
    devices_t &container;
    Fl_Widget *add_button;
};

auto static make_device_id(my_table_t &container) -> widgetable_ptr_t {
    struct widget_t final : table_widget::input_t {
        using parent_t = table_widget::input_t;
        using parent_t::parent_t;

        Fl_Widget *create_widget(int x, int y, int w, int h) override {
            auto r = parent_t::create_widget(x, y, w, h);
            input->callback([](auto, void *data) { reinterpret_cast<my_table_t *>(data)->refresh(); }, &container);
            input->when(input->when() | FL_WHEN_CHANGED);
            return r;
        }

        bool store(void *data) override {
            auto ctx = reinterpret_cast<serialization_context_t *>(data);
            ctx->device_id = input->value();
            return true;
        }
    };
    return new widget_t(container);
}

auto static make_label(my_table_t &container) -> widgetable_ptr_t {
    struct widget_t final : table_widget::input_t {
        using parent_t = table_widget::input_t;
        using parent_t::parent_t;

        Fl_Widget *create_widget(int x, int y, int w, int h) override {
            auto r = parent_t::create_widget(x, y, w, h);
            input->callback([](auto, void *data) { reinterpret_cast<my_table_t *>(data)->refresh(); }, &container);
            input->when(input->when() | FL_WHEN_CHANGED);
            return r;
        }

        bool store(void *data) override {
            auto ctx = reinterpret_cast<serialization_context_t *>(data);
            ctx->label = input->value();
            return true;
        }
    };
    return new widget_t(container);
}

auto static make_notice(my_table_t &container) -> widgetable_ptr_t {
    struct widget_t final : table_widget::label_t {
        using parent_t = table_widget::label_t;
        using parent_t::parent_t;

        void reset() override {
            auto label = static_cast<my_table_t &>(container).error;
            auto ptr = label.size() ? label.data() : "";
            input->label(ptr);
        }
    };
    return new widget_t(container);
}

auto static make_actions(my_table_t &container) -> widgetable_ptr_t {
    struct widget_t final : widgetable_t {
        using parent_t = widgetable_t;
        using parent_t::parent_t;

        Fl_Widget *create_widget(int x, int y, int w, int h) override {
            auto group = new Fl_Group(x, y, w, h);
            group->begin();
            group->box(FL_FLAT_BOX);
            auto yy = y + padding, ww = 160, hh = h - padding * 2;
            auto add = new Fl_Button(x + padding, yy, ww, hh, "add new device");
            add->callback([](auto, void *data) { static_cast<my_table_t *>(data)->add_new_device(); }, &container);
            add->deactivate();

            group->resizable(nullptr);

            group->end();
            widget = group;

            this->reset();
            auto &container = static_cast<my_table_t &>(this->container);
            container.add_button = add;
            return widget;
        }
    };

    return new widget_t(container);
}

} // namespace

devices_t::devices_t(app_supervisor_t &supervisor, Fl_Tree *tree) : parent_t(supervisor, tree, false) {
    supervisor.set_devices(this);
    update_label();
}

bool devices_t::on_select() {
    content = supervisor.replace_content([&](content_t *content) -> content_t * {
        auto prev = content->get_widget();
        return new my_table_t(*this, prev->x(), prev->y(), prev->w(), prev->h());
    });
    return true;
}

void devices_t::update_label() {
    auto devices_count = std::size_t{0};
    auto cluster = supervisor.get_cluster();
    if (cluster) {
        devices_count = cluster->get_devices().size();
        if (devices_count > 1) {
            --devices_count; // dont count self
        }
    }
    auto l = fmt::format("devices ({})", devices_count);
    this->label(l.data());
}

auto devices_t::set_self(model::device_t &self) -> augmentation_ptr_t {
    return within_tree([&]() {
        auto self_node = new tree_item::self_device_t(self, supervisor, tree());
        add(prefs(), "self", self_node);
        tree()->close(self_node, 0);
        return self_node->get_proxy();
    });
}

augmentation_ptr_t devices_t::add_peer(model::device_t &peer) {
    update_label();
    return within_tree([&]() { return insert_by_label(new peer_device_t(peer, supervisor, tree()), 1)->get_proxy(); });
}

peer_device_t *devices_t::get_peer(const model::device_t &peer) {
    for (int i = 1; i < children(); ++i) {
        auto node = static_cast<peer_device_t *>(child(i));
        if (node->peer == peer) {
            return node;
        }
    }
    supervisor.get_logger()->warn("no node/tree-item for device {}", peer.device_id().get_short());
    return {};
}

void devices_t::remove_child(tree_item_t *child) {
    parent_t::remove_child(child);
    update_label();
}


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

#pragma once

#include "model/device.h"
#include "../tree_item.h"
#include "peer_device.h"

namespace syncspirit::fltk::tree_item {

struct devices_t : tree_item_t {
    using parent_t = tree_item_t;
    devices_t(app_supervisor_t &supervisor, Fl_Tree *tree);

    augmentation_ptr_t set_self(model::device_t &self);
    augmentation_ptr_t add_peer(model::device_t &peer);
    peer_device_t *get_peer(const model::device_t &peer);

    bool on_select() override;
    void update_label() override;
    void remove_child(tree_item_t *child) override;
};

} // namespace syncspirit::fltk::tree_item