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

#pragma once

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

namespace syncspirit::fltk::config {

namespace bfs = std::filesystem;

namespace impl {
struct positive_integer_t : property_t {
    positive_integer_t(std::string label, std::string explanation, std::uint64_t value, std::uint64_t default_value);

    error_ptr_t validate_value() noexcept override;

    std::uint64_t native_value;
};

struct string_t : property_t {
    string_t(std::string label, std::string explanation, std::string value, std::string default_value);
};

struct url_t : string_t {
    using parent_t = string_t;
    using parent_t::parent_t;
    error_ptr_t validate_value() noexcept override;
};

struct path_t : property_t {
    path_t(std::string label, std::string explanation, std::string value, std::string default_value,
           property_kind_t kind = property_kind_t::file);

    bfs::path convert() noexcept;
};

struct bool_t : property_t {
    bool_t(bool value, bool default_value, std::string label = "enabled");
    error_ptr_t validate_value() noexcept override;

    std::uint64_t native_value;
};

struct log_sink_t : property_t {
    log_sink_t(std::string name, std::vector<std::string> sinks, spdlog::level::level_enum level);
    void reflect_to(syncspirit::config::main_t &main) override;

    std::vector<std::string> sinks;
    spdlog::level::level_enum level;
};

} // namespace impl

namespace bep {

struct blocks_max_requested_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    blocks_max_requested_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

struct blocks_simultaneous_write_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    blocks_simultaneous_write_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

struct connect_timeout_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    connect_timeout_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

struct advances_per_iteration_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    advances_per_iteration_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

struct request_timeout_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    request_timeout_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

struct rx_buff_size_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    rx_buff_size_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

struct rx_timeout_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    rx_timeout_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

struct tx_buff_limit_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    tx_buff_limit_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

struct tx_timeout_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    tx_timeout_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

struct stats_interval_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    stats_interval_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

} // namespace bep

namespace db {

struct max_blocks_per_diff_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    max_blocks_per_diff_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

struct max_files_per_diff_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    max_files_per_diff_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

struct uncommitted_threshold_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    uncommitted_threshold_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

struct upper_limit_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    upper_limit_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

} // namespace db

namespace dialer {

struct enabled_t final : impl::bool_t {
    using parent_t = impl::bool_t;
    using parent_t::parent_t;

    void reflect_to(syncspirit::config::main_t &main) override;
};

struct redial_timeout_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    redial_timeout_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

struct skip_discovers_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    skip_discovers_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

} // namespace dialer

namespace fs {

struct bytes_scan_iteration_limit_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    bytes_scan_iteration_limit_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

struct files_scan_iteration_limit_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    files_scan_iteration_limit_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

struct mru_size_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    mru_size_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

struct temporally_timeout_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    temporally_timeout_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

} // namespace fs

namespace global_discovery {

struct enabled_t final : impl::bool_t {
    using parent_t = impl::bool_t;
    using parent_t::parent_t;

    void reflect_to(syncspirit::config::main_t &main) override;
};

struct debug_t final : impl::bool_t {
    using parent_t = impl::bool_t;
    using parent_t::parent_t;

    debug_t(bool value, bool default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

struct announce_url_t final : impl::url_t {
    using parent_t = impl::url_t;

    static const char *explanation_;

    announce_url_t(std::string value, std::string default_value);

    void reflect_to(syncspirit::config::main_t &main) override;
};

struct cert_file_t final : impl::path_t {
    using parent_t = impl::path_t;

    static const char *explanation_;

    cert_file_t(std::string value, std::string default_value);

    void reflect_to(syncspirit::config::main_t &main) override;
};

struct device_id_t final : impl::string_t {
    using parent_t = impl::string_t;

    static const char *explanation_;

    device_id_t(std::string value, std::string default_value);
    error_ptr_t validate_value() noexcept override;

    void reflect_to(syncspirit::config::main_t &main) override;
};

struct key_file_t final : impl::path_t {
    using parent_t = impl::path_t;

    static const char *explanation_;

    key_file_t(std::string value, std::string default_value);

    void reflect_to(syncspirit::config::main_t &main) override;
};

struct rx_buff_size_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    rx_buff_size_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

struct timeout_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    timeout_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

} // namespace global_discovery

namespace local_discovery {

struct enabled_t final : impl::bool_t {
    using parent_t = impl::bool_t;
    using parent_t::parent_t;

    void reflect_to(syncspirit::config::main_t &main) override;
};

struct frequency_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    frequency_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

struct port_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    port_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

} // namespace local_discovery

namespace main {

struct default_location_t final : impl::path_t {
    using parent_t = impl::path_t;

    static const char *explanation_;

    default_location_t(std::string value, std::string default_value);

    void reflect_to(syncspirit::config::main_t &main) override;
};

struct device_name_t final : impl::string_t {
    using parent_t = impl::string_t;

    static const char *explanation_;

    device_name_t(std::string value, std::string default_value);

    void reflect_to(syncspirit::config::main_t &main) override;
};

struct hasher_threads_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    hasher_threads_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

struct timeout_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    timeout_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

} // namespace main

namespace relay {

struct enabled_t final : impl::bool_t {
    using parent_t = impl::bool_t;
    using parent_t::parent_t;

    void reflect_to(syncspirit::config::main_t &main) override;
};

struct debug_t final : impl::bool_t {
    using parent_t = impl::bool_t;
    using parent_t::parent_t;

    debug_t(bool value, bool default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

struct discovery_url_t final : impl::url_t {
    using parent_t = impl::url_t;

    static const char *explanation_;

    discovery_url_t(std::string value, std::string default_value);

    void reflect_to(syncspirit::config::main_t &main) override;
};

struct rx_buff_size_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    rx_buff_size_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

} // namespace relay

namespace upnp {

struct enabled_t final : impl::bool_t {
    using parent_t = impl::bool_t;
    using parent_t::parent_t;

    void reflect_to(syncspirit::config::main_t &main) override;
};

struct debug_t final : impl::bool_t {
    using parent_t = impl::bool_t;

    debug_t(bool value, bool default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

struct external_port_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    external_port_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

struct max_wait_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    max_wait_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

struct rx_buff_size_t final : impl::positive_integer_t {
    using parent_t = impl::positive_integer_t;

    static const char *explanation_;

    rx_buff_size_t(std::uint64_t value, std::uint64_t default_value);
    void reflect_to(syncspirit::config::main_t &main) override;
};

} // namespace upnp

} // namespace syncspirit::fltk::config


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

#include "property.h"

using namespace syncspirit::fltk::config;

property_t::property_t(std::string label_, std::string explanation_, std::string value_, std::string default_value_,
                       property_kind_t kind_)
    : label{label_}, explanation{explanation_}, value{value_}, initial_value{value_}, default_value{default_value_},
      kind{kind_} {}

std::string_view property_t::get_label() const noexcept { return label; }

std::string_view property_t::get_explanation() const noexcept { return explanation; }

std::string_view property_t::get_value() const noexcept { return value; }

property_kind_t property_t::get_kind() const noexcept { return kind; }

void property_t::set_value(std::string_view value_) noexcept {
    value = value_;
    error = validate_value();
}

void property_t::reset() noexcept {
    value = default_value;
    error = {};
}

void property_t::undo() noexcept {
    value = initial_value;
    error = {};
}

const error_ptr_t &property_t::validate() noexcept { return error; }

error_ptr_t property_t::validate_value() noexcept { return {}; }

bool property_t::same_as_initial() const noexcept { return initial_value == value; }

bool property_t::same_as_default() const noexcept { return default_value == value; }

repo_name: basiliscos/syncspirit

license: mit


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

#pragma once

#include "config/main.h"
#include <string>
#include <memory>
#include <boost/intrusive_ptr.hpp>
#include <boost/smart_ptr/intrusive_ref_counter.hpp>

namespace syncspirit::fltk::config {

using main_cfg_t = syncspirit::config::main_t;
using error_ptr_t = std::unique_ptr<std::string>;

enum class property_kind_t { text, file, directory, positive_integer, boolean, log_sink };

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

    std::string_view get_label() const noexcept;
    std::string_view get_explanation() const noexcept;
    std::string_view get_value() const noexcept;
    property_kind_t get_kind() const noexcept;
    void set_value(std::string_view) noexcept;
    void reset() noexcept;
    void undo() noexcept;
    const error_ptr_t &validate() noexcept;
    bool same_as_initial() const noexcept;
    bool same_as_default() const noexcept;
    virtual void reflect_to(syncspirit::config::main_t &main) = 0;

  protected:
    property_t(std::string label, std::string explanation, std::string value, std::string default_value,
               property_kind_t kind);
    virtual error_ptr_t validate_value() noexcept;

    std::string label;
    std::string explanation;
    std::string value;
    std::string initial_value;
    std::string default_value;
    property_kind_t kind;
    error_ptr_t error;
};

using property_ptr_t = boost::intrusive_ptr<property_t>;

} // namespace syncspirit::fltk::config


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

#include "table.h"

#include <FL/fl_draw.H>
#include <FL/Fl_Int_Input.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Group.H>
#include <FL/Fl_Check_Button.H>
#include <FL/Fl_Native_File_Chooser.H>
#include <filesystem>
#include <spdlog/fmt/fmt.h>
#include <boost/nowide/convert.hpp>

#include <cassert>

using namespace syncspirit::fltk::config;
namespace bfs = std::filesystem;

static constexpr int col_min_size = 60;

table_t::clip_guard_t::clip_guard_t(int, int x, int y, int w_, int h) : w{w_} {
    if (w) {
        fl_push_clip(x, y, w, h);
    }
}

table_t::clip_guard_t::~clip_guard_t() {
    if (w) {
        fl_pop_clip();
    }
}

auto table_t::cell_t::clip(int col, int x, int y, int w, int h) -> clip_guard_t {
    return clip_guard_t(col, x, y, w, h);
}

void table_t::cell_t::resize(int, int, int, int) {}
void table_t::cell_t::load_value() {}

struct category_cell_t final : table_t::cell_t {
    using parent_t = table_t::cell_t;

    category_cell_t(table_t *table_, category_ptr_t category_) : table{table_}, category{std::move(category_)} {}

    auto clip(int col, int x, int y, int w, int h) -> table_t::clip_guard_t override {
        return col == 0 ? parent_t::clip(col, x, y, w, h) : table_t::clip_guard_t(0, 0, 0, 0, 0);
    }

    void draw(int col, int x, int y, int w, int h) override {
        Fl_Align align = FL_ALIGN_RIGHT;
        const char *data = nullptr;

        bool restore_font = false;
        int offset = 5;
        int dw = 0;
        int text_dw = 0;
        auto bg_color = FL_GRAY0;
        switch (col) {
        case 0:
            data = category->get_label().data();
            text_dw = -offset;
            break;
        case 1:
            fl_font(FL_HELVETICA, 14);
            data = category->get_explanation().data();
            dw = table->col_width(2) + table->col_width(3);
            bg_color = FL_BLUE;
            align = FL_ALIGN_RIGHT;
            text_dw = -offset;
            restore_font = true;
            break;
        default:
            return;
        }

        fl_color(bg_color);
        fl_rectf(x, y, w + dw, h);
        fl_color(FL_WHITE);
        if (data) {
            fl_draw(data, x + offset, y, w - offset + dw + text_dw, h, align);
        }
        if (restore_font) {
            fl_font(FL_HELVETICA, 16);
        }
    }

    table_t *table;
    category_ptr_t category;
};

struct property_cell_t : table_t::cell_t {
    using parent_t = table_t::cell_t;

    property_cell_t(table_t *table_, property_ptr_t property_, int row_)
        : table{table_}, property{std::move(property_)}, row{row_}, editing{false} {

        int xx, yy, ww, hh;
        table->find_cell(table_t::CONTEXT_TABLE, row, 2, xx, yy, ww, hh);
        group_buttons = new Fl_Group(xx, yy, ww, hh);
        group_buttons->box(FL_FLAT_BOX);

        group_buttons->begin();
        int padding = 2;
        auto button_w = ww / 2 - padding * 3;
        auto button_h = hh - padding * 2;

        button_undo = new Fl_Button(xx + padding, yy + padding, button_w, button_h, "@undo");
        button_undo->callback(
            [](Fl_Widget *, void *data) {
                auto self = static_cast<property_cell_t *>(data);
                self->property->undo();
                self->load_value();
            },
            this);
        button_undo->copy_tooltip("undo to initial");

        button_reset = new Fl_Button(xx + padding + button_w + padding, yy + padding, button_w, button_h, "@refresh");
        button_reset->callback(
            [](Fl_Widget *, void *data) {
                auto self = static_cast<property_cell_t *>(data);
                self->property->reset();
                self->load_value();
            },
            this);
        button_reset->copy_tooltip("reset to default");

        group_buttons->end();
        group_buttons->resizable(group_buttons);

        update_buttons();
    }

    void draw(int col, int x, int y, int w, int h) override {
        switch (col) {
        case 0:
            draw_label(x, y, w, h);
            break;
        case 3:
            draw_explanation_or_error(x, y, w, h);
            break;
        }
    }

    void draw_label(int x, int y, int w, int h) {
        auto label = property->get_label();
        fl_color(FL_BLACK);
        fl_draw(label.data(), x, y, w, h, FL_ALIGN_LEFT);
    }

    void update_buttons() {
        if (property->same_as_initial()) {
            button_undo->deactivate();
        } else {
            button_undo->activate();
        }

        if (property->same_as_default()) {
            button_reset->deactivate();
        } else {
            button_reset->activate();
        }
    }

    void load_value() override { done_editing(); }

    void done_editing() {
        update_buttons();
        table->done_editing();
    }

    void draw_explanation_or_error(int x, int y, int w, int h) {
        auto &err = property->validate();
        const char *text;
        if (err) {
            fl_color(FL_RED);
            fl_rectf(x, y, w, h);
            fl_color(FL_BLACK);
            text = err->c_str();
        } else {
            text = property->get_explanation().data();
        }
        fl_color(FL_BLACK);
        fl_draw(text, x, y, w, h, FL_ALIGN_LEFT);
    }

    void resize(int, int, int, int) override {
        [&]() -> void {
            int xx, yy, ww, hh;
            table->find_cell(table_t::CONTEXT_TABLE, row, 2, xx, yy, ww, hh);
            group_buttons->resize(xx, yy, ww, hh);
        }();
        [&]() -> void {
            int xx, yy, ww, hh;
            table->find_cell(table_t::CONTEXT_TABLE, row, 1, xx, yy, ww, hh);
            input_generic->resize(xx, yy, ww, hh);
        }();
    }

    table_t *table;
    property_ptr_t property;
    int row;
    bool editing;
    Fl_Widget *input_generic;
    Fl_Group *group_buttons;
    Fl_Button *button_undo;
    Fl_Button *button_reset;
};

struct int_cell_t final : property_cell_t {
    using parent_t = property_cell_t;

    int_cell_t(table_t *table_, property_ptr_t property_, int row_) : parent_t(table_, std::move(property_), row_) {
        int xx, yy, ww, hh;
        table->find_cell(table_t::CONTEXT_TABLE, row, 1, xx, yy, ww, hh);
        auto input = new Fl_Int_Input(xx, yy, ww, hh);
        input->callback([](Fl_Widget *, void *data) { reinterpret_cast<int_cell_t *>(data)->save_input(); }, this);
        input_generic = input;
        load_value();
    }

    void load_value() {
        auto value = property->get_value();
        static_cast<Fl_Int_Input *>(input_generic)->value(value.data());
        parent_t::load_value();
    }

    void save_input() {
        auto value = static_cast<Fl_Int_Input *>(input_generic)->value();
        property->set_value(value);
        done_editing();
    }
};

struct bool_cell_t final : property_cell_t {
    using parent_t = property_cell_t;

    bool_cell_t(table_t *table_, property_ptr_t property_, int row_) : parent_t(table_, std::move(property_), row_) {
        int xx, yy, ww, hh;
        table->find_cell(table_t::CONTEXT_TABLE, row, 1, xx, yy, ww, hh);
        auto input = new Fl_Check_Button(xx, yy, ww, hh);
        input->callback([](Fl_Widget *, void *data) { reinterpret_cast<bool_cell_t *>(data)->save_input(); }, this);
        input_generic = input;
        load_value();
    }

    void load_value() override {
        auto value = property->get_value();
        static_cast<Fl_Check_Button *>(input_generic)->value(value == "true");
        parent_t::load_value();
    }

    void save_input() {
        auto value = static_cast<Fl_Check_Button *>(input_generic)->value();
        property->set_value(value ? "true" : "");
        done_editing();
    }
};

struct string_cell_t final : property_cell_t {
    using parent_t = property_cell_t;

    string_cell_t(table_t *table_, property_ptr_t property_, int row_) : parent_t(table_, std::move(property_), row_) {
        int xx, yy, ww, hh;
        table->find_cell(table_t::CONTEXT_TABLE, row, 1, xx, yy, ww, hh);
        auto input = new Fl_Input(xx, yy, ww, hh);
        input->callback([](Fl_Widget *, void *data) { reinterpret_cast<string_cell_t *>(data)->save_input(); }, this);
        input_generic = input;
        load_value();
    }

    void load_value() override {
        auto value = property->get_value();
        static_cast<Fl_Input *>(input_generic)->value(value.data());
        parent_t::load_value();
    }

    void save_input() {
        auto value = static_cast<Fl_Input *>(input_generic)->value();
        property->set_value(value);
        done_editing();
    }
};

struct path_cell_t final : property_cell_t {
    using parent_t = property_cell_t;

    path_cell_t(table_t *table_, property_ptr_t property_, int row_) : parent_t(table_, std::move(property_), row_) {
        int xx, yy, ww, hh;
        auto w = 25;
        auto p = 2;
        table->find_cell(table_t::CONTEXT_TABLE, row, 1, xx, yy, ww, hh);
        auto group = new Fl_Group(xx, yy, ww, hh);
        group->begin();
        auto input = new Fl_Input(xx, yy, ww - (w + p * 2), hh);
        input->callback([](Fl_Widget *, void *data) { reinterpret_cast<path_cell_t *>(data)->save_input(); }, this);

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

        group->end();
        input_generic = group;
        load_value();
    }

    void load_value() override {
        auto value = property->get_value();
        auto child_control = static_cast<Fl_Group *>(input_generic)->child(0);
        static_cast<Fl_Input *>(child_control)->value(value.data());
        parent_t::load_value();
    }

    void save_input() {
        auto child_control = static_cast<Fl_Group *>(input_generic)->child(0);
        auto value = static_cast<Fl_Input *>(child_control)->value();
        property->set_value(value);
        done_editing();
    }

    void on_click() {
        using T = Fl_Native_File_Chooser::Type;
        auto k = property->get_kind();
        auto type = k == property_kind_t::file ? T::BROWSE_FILE : T::BROWSE_DIRECTORY;

        Fl_Native_File_Chooser file_chooser;
        file_chooser.title(property->get_explanation().data());
        file_chooser.type(type);
        if (k == property_kind_t::file) {
            auto path = bfs::path(boost::nowide::widen(property->get_value()));
            auto parent = boost::nowide::narrow(path.parent_path().wstring());
            file_chooser.directory(parent.data());

            auto ext = fmt::format("*{}", boost::nowide::narrow(path.extension().wstring()));
            auto filter = fmt::format("{}\t{}", ext, ext);
            file_chooser.filter(filter.data());
        }

        auto r = file_chooser.show();
        if (r != 0) {
            return;
        }

        // commit
        auto path = bfs::path(boost::nowide::widen(file_chooser.filename()));
        property->set_value(boost::nowide::narrow(path.wstring()));
        load_value();
        done_editing();
    }
};

struct log_cell_t final : property_cell_t {
    using parent_t = property_cell_t;

    log_cell_t(table_t *table_, property_ptr_t property_, int row_) : parent_t(table_, std::move(property_), row_) {
        group_buttons->hide();
        int xx, yy, ww, hh;
        table->find_cell(table_t::CONTEXT_TABLE, row, 1, xx, yy, ww, hh);
        auto input = new Fl_Input(xx, yy, ww, hh);
        input->deactivate();
        input_generic = input;
        load_value();
    }

    void load_value() override {
        auto value = property->get_value();
        static_cast<Fl_Input *>(input_generic)->value(value.data());
        parent_t::load_value();
    }
};

table_t::table_t(const categories_t &categories_, int x, int y, int w, int h)
    : parent_t(x, y, w, h), categories{categories_} {

    row_header(0);
    row_height_all(20);
    row_resize(0);
    cols(4);
    col_header(1);
    col_width(0, 220);
    col_width(1, 570);
    col_width(2, 60);
    col_width(3, w / 2);
    col_resize(1);

    when(FL_WHEN_CHANGED | when());
    resizable(this);

    end();

    create_cells();
}

void 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);
        return;
    }
    case CONTEXT_CELL:
        draw_data(row, col, x, y, w, h);
        return;
    case CONTEXT_COL_HEADER:
        draw_header(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);
        }

        for (size_t i = 0; i < cells.size(); ++i) {
            cells.at(i)->resize(x, y, w, h);
        }
        init_sizes();
        return;
    }
    default:
        return;
    }
}

void table_t::draw_header(int, int x, int y, int w, int h) {
    fl_push_clip(x, y, w, h);
    fl_draw_box(FL_THIN_UP_BOX, x, y, w, h, row_header_color());
    fl_pop_clip();
}

void table_t::draw_data(int r, int col, int x, int y, int w, int h) {
    auto &cell = cells.at(static_cast<size_t>(r));
    auto guard = cell->clip(col, x, y, w, h);
    cell->draw(col, x, y, w, h);
}

void table_t::create_cells() {
    int total_rows = 0;
    for (auto &c : categories) {
        ++total_rows;
        for (auto &_ : c->get_properties()) {
            (void)_;
            ++total_rows;
        }
    }

    rows(total_rows);
    int row = 0;
    begin();
    for (auto &c : categories) {
        cells.push_back(cell_ptr_t(new category_cell_t(this, c)));
        row++;
        for (auto &p : c->get_properties()) {
            auto cell_ptr = [&]() -> property_cell_t * {
                auto k = p->get_kind();
                if (k == property_kind_t::positive_integer) {
                    return new int_cell_t(this, p, row);
                } else if (k == property_kind_t::boolean) {
                    return new bool_cell_t(this, p, row);
                } else if (k == property_kind_t::text) {
                    return new string_cell_t(this, p, row);
                } else if (k == property_kind_t::file || k == property_kind_t::directory) {
                    return new path_cell_t(this, p, row);
                } else if (k == property_kind_t::log_sink) {
                    return new log_cell_t(this, p, row);
                }
                assert(0 && "should not happen");
                return nullptr;
            }();
            ++row;
            cells.push_back(cell_ptr_t(cell_ptr));
        }
    }
    end();
}

void table_t::done_editing() {
    if (auto cb = callback(); cb) {
        Fl_Widget::do_callback();
    };
    redraw();
}

void table_t::reload_values() {
    for (auto &c : cells) {
        c->load_value();
    }
    redraw();
}


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

#pragma once

#include "category.h"
#include <FL/Fl_Table.H>
#include <memory>
#include <vector>

namespace syncspirit::fltk::config {

struct table_t : Fl_Table {
    using parent_t = Fl_Table;

    struct clip_guard_t {
        clip_guard_t(int col, int x, int y, int w, int h);
        ~clip_guard_t();
        int w;
    };

    struct cell_t {
        virtual ~cell_t() = default;
        virtual clip_guard_t clip(int col, int x, int y, int w, int h);
        virtual void draw(int col, int x, int y, int w, int h) = 0;
        virtual void resize(int x, int y, int w, int h);
        virtual void load_value();
    };

    using cell_ptr_t = std::unique_ptr<cell_t>;
    using cells_t = std::vector<cell_ptr_t>;
    using parent_t::find_cell;
    using parent_t::tih;
    using parent_t::tiy;

    table_t(const categories_t &categories, 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 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);
    void create_cells();
    void reload_values();
    void done_editing();

    categories_t categories;
    cells_t cells;
};

} // namespace syncspirit::fltk::config


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

#include "content.h"

using namespace syncspirit::fltk;

void content_t::refresh() {}
void content_t::reset() {}
bool content_t::store(void *) { return true; }


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

#pragma once

#include <FL/Fl_Widget.H>

#include <type_traits>

namespace syncspirit::fltk {

struct content_t {
    virtual Fl_Widget *get_widget() = 0;

    virtual void refresh();
    virtual void reset();
    virtual bool store(void *);

    virtual ~content_t() = default;
};

template <typename T, typename E = void> struct contentable_t : T, content_t {
    using T::T;

    Fl_Widget *get_widget() override { return this; }
};

template <typename T> struct contentable_t<T, std::enable_if_t<std::is_base_of_v<Fl_Group, T>>> : T, content_t {
    using T::T;

    Fl_Widget *get_widget() override { return this; }

    void refresh() override {
        for (int i = 0; i < this->children(); ++i) {
            auto child = this->child(i);
            if (auto content = dynamic_cast<content_t *>(child)) {
                content->refresh();
            }
        }
    }
};

} // namespace syncspirit::fltk


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

#include "file_table.h"

#include "table_widget/checkbox.h"
#include "utils.hpp"
#include "proto/proto-helpers-bep.h"
#include "presentation/cluster_file_presence.h"

#include <memory_resource>
#include <algorithm>

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

using F = presence_t::features_t;

static constexpr size_t max_history_records = 5;

namespace {

struct ro_checkbox_t : table_widget::checkbox_t {
    using parent_t = table_widget::checkbox_t;

    ro_checkbox_t(Fl_Widget &container, bool value_) : parent_t(container), value{value_} {}

    Fl_Widget *create_widget(int x, int y, int w, int h) override {
        auto widget = parent_t::create_widget(x, y, w, h);
        input->deactivate();
        input->value(value ? 1 : 0);
        return widget;
    }

    bool value;
};

auto make_checkbox(Fl_Widget &container, bool value) -> widgetable_ptr_t { return new ro_checkbox_t(container, value); }

} // namespace

static presence_t *resolve_presence(presence_t *source) {
    if (source->get_features() & F::missing) {
        auto best = source->get_entity()->get_best();
        return const_cast<presence_t *>(best);
    }
    return source;
}

file_table_t::file_table_t(presence_item_t &container_, int x, int y, int w, int h)
    : parent_t(x, y, w, h), container{container_}, displayed_versions{0} {

    auto presence = resolve_presence(&container.get_presence());
    assert(presence->get_features() & F::cluster);
    auto cluster_presence = static_cast<cluster_file_presence_t *>(presence);
    auto &entity = cluster_presence->get_file_info();
    auto data = table_rows_t();

    name_cell = new static_string_provider_t("");
    device_cell = new static_string_provider_t("");
    modified_cell = new static_string_provider_t("");
    sequence_cell = new static_string_provider_t("");
    size_cell = new static_string_provider_t("");
    block_size_cell = new static_string_provider_t("");
    blocks_cell = new static_string_provider_t("");
    permissions_cell = new static_string_provider_t("");
    modified_s_cell = new static_string_provider_t("");
    modified_ns_cell = new static_string_provider_t("");
    symlink_target_cell = new static_string_provider_t("");
    entries_cell = new static_string_provider_t("");
    entries_size_cell = new static_string_provider_t("");
    local_entries_cell = new static_string_provider_t("");

    data.push_back({"name", name_cell});
    data.push_back({"device", device_cell});
    data.push_back({"modified", modified_cell});
    data.push_back({"sequence", sequence_cell});
    data.push_back({"size", size_cell});
    data.push_back({"block size", block_size_cell});
    data.push_back({"blocks", blocks_cell});
    data.push_back({"permissions", permissions_cell});
    data.push_back({"modified_s", modified_s_cell});
    data.push_back({"modified_ns", modified_ns_cell});
    data.push_back({"is_directory", make_checkbox(*this, entity.is_dir())});
    data.push_back({"is_file", make_checkbox(*this, entity.is_file())});
    data.push_back({"is_link", make_checkbox(*this, entity.is_link())});
    data.push_back({"is_invalid", make_checkbox(*this, entity.is_invalid())});
    data.push_back({"is_deleted", make_checkbox(*this, entity.is_deleted())});
    data.push_back({"no_permissions", make_checkbox(*this, entity.has_no_permissions())});
    data.push_back({"symlink_target", symlink_target_cell});
    data.push_back({"entries", entries_cell});
    data.push_back({"entries size", entries_size_cell});
    data.push_back({"cluster/local entries", local_entries_cell});

    assign_rows(std::move(data));

    refresh();
}

void file_table_t::refresh() {
    using allocator_t = std::pmr::polymorphic_allocator<char>;
    using counters_t = std::pmr::vector<proto::Counter>;
    auto buffer = std::array<std::byte, 1024>();
    auto pool = std::pmr::monotonic_buffer_resource(buffer.data(), buffer.size());
    auto allocator = allocator_t(&pool);

    auto presence = resolve_presence(&container.get_presence());
    assert(presence->get_features() & F::cluster);
    auto cluster_presence = static_cast<cluster_file_presence_t *>(presence);
    auto &file_info = cluster_presence->get_file_info();
    auto device = file_info.get_folder_info()->get_device();
    auto &devices = container.supervisor.get_cluster()->get_devices();
    auto data = table_rows_t();
    auto modified_s = file_info.get_modified_s();
    auto modified_date = model::pt::from_time_t(modified_s);
    auto version = file_info.get_version();
    auto &stats = presence->get_stats();

    name_cell->update(file_info.get_name());
    device_cell->update(fmt::format("{} ({})", device->get_name(), device->device_id().get_short()));
    modified_cell->update(model::pt::to_simple_string(modified_date));
    sequence_cell->update(fmt::format("{}", file_info.get_sequence()));
    size_cell->update(get_file_size(file_info.get_size()));

    for (int i = 0; i < static_cast<int>(displayed_versions); ++i) {
        remove_row(5);
    }

    auto &model_counters = version->get_counters();
    auto sorted_counters = counters_t(model_counters.begin(), model_counters.end(), allocator);
    auto sorter = [](const proto::Counter &lhs, const proto::Counter &rhs) -> bool {
        return proto::get_value(lhs) > proto::get_value(rhs);
    };
    std::sort(sorted_counters.begin(), sorted_counters.end(), sorter);

    displayed_versions = std::min(version->counters_size(), max_history_records);
    for (size_t i = 0; i < displayed_versions; ++i) {
        auto &counter = sorted_counters[i];
        auto value = proto::get_value(counter);
        auto device_id = proto::get_id(counter);
        auto device_short = model::device_id_t::make_short(device_id);
        auto device_str = std::pmr::string(allocator);
        for (auto &it : devices) {
            auto &peer = *it.item;
            auto &peer_id = peer.device_id();
            if (peer.device_id().get_uint() == device_id) {
                fmt::format_to(std::back_inserter(device_str), "{}, {}", peer.get_name(), device_short);
                break;
            }
        }
        if (device_str.empty()) {
            device_str = std::pmr::string(device_short, allocator);
        }

        auto label = std::pmr::string(allocator);
        fmt::format_to(std::back_inserter(label), "({}) {}", value, device_str);
        insert_row("modification", new static_string_provider_t(std::move(label)), 5 + i);
    }

    block_size_cell->update(std::to_string(file_info.get_block_size()));
    blocks_cell->update(std::to_string(file_info.get_blocks().size()));
    permissions_cell->update(fmt::format("0{:o}", file_info.get_permissions()));
    modified_s_cell->update(fmt::format("{}", modified_s));
    modified_ns_cell->update(fmt::format("{}", file_info.get_modified_ns()));
    symlink_target_cell->update(file_info.get_link_target());
    entries_cell->update(fmt::format("{}", stats.entities));
    entries_size_cell->update(get_file_size(stats.size));
    local_entries_cell->update(fmt::format("{}/{}", stats.cluster_entries, stats.entities));

    redraw();
}


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

#pragma once

#include "static_table.h"
#include "presence_item.h"

namespace syncspirit::fltk::content {

struct file_table_t : static_table_t {
    using parent_t = static_table_t;

    file_table_t(presence_item_t &container_, int x, int y, int w, int h);

    void refresh() override;

  private:
    presence_item_t &container;
    static_string_provider_ptr_t name_cell;
    static_string_provider_ptr_t device_cell;
    static_string_provider_ptr_t modified_cell;
    static_string_provider_ptr_t sequence_cell;
    static_string_provider_ptr_t size_cell;
    static_string_provider_ptr_t block_size_cell;
    static_string_provider_ptr_t blocks_cell;
    static_string_provider_ptr_t permissions_cell;
    static_string_provider_ptr_t modified_s_cell;
    static_string_provider_ptr_t modified_ns_cell;
    static_string_provider_ptr_t symlink_target_cell;
    static_string_provider_ptr_t entries_cell;
    static_string_provider_ptr_t entries_size_cell;
    static_string_provider_ptr_t local_entries_cell;
    size_t displayed_versions;
};

} // namespace syncspirit::fltk::content


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

#include "folder_table.h"

#include "model/diff/modify/remove_folder.h"
#include "model/diff/modify/remove_blocks.h"
#include "model/diff/modify/share_folder.h"
#include "model/diff/modify/suspend_folder.h"
#include "model/diff/modify/unshare_folder.h"
#include "model/diff/modify/upsert_folder.h"
#include "model/diff/local/scan_request.h"

#include "../table_widget/checkbox.h"
#include "../table_widget/choice.h"
#include "../table_widget/input.h"
#include "../table_widget/int_input.h"
#include "../table_widget/label.h"
#include "../table_widget/path.h"

#include "utils/format.hpp"

#include <FL/fl_ask.H>
#include <boost/nowide/convert.hpp>
#include <charconv>

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

static constexpr int padding = 2;

namespace {

using ctx_t = folder_table_t::serialization_context_t;

struct device_share_widget_t final : widgetable_t {
    using parent_t = widgetable_t;
    device_share_widget_t(folder_table_t &container, model::device_ptr_t device_, bool disabled_);

    Fl_Widget *create_widget(int x, int y, int w, int h) override;
    void reset() override;
    bool store(void *data) override;

    model::device_ptr_t initial_device;
    model::device_ptr_t device;
    Fl_Choice *input;
    bool disabled;
};

device_share_widget_t::device_share_widget_t(folder_table_t &container, model::device_ptr_t device_, bool disabled_)
    : parent_t(container), initial_device{device_}, device{device_}, input{nullptr}, disabled{disabled_} {}

Fl_Widget *device_share_widget_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(300, ww);

    input = new Fl_Choice(x + padding, yy, ww, hh);
    auto add = new Fl_Button(input->x() + input->w() + padding * 2, yy, hh, hh, "@+");
    auto remove = new Fl_Button(add->x() + add->w() + padding * 2, yy, hh, hh, "@undo");

    add->callback(
        [](auto, void *data) {
            auto self = reinterpret_cast<device_share_widget_t *>(data);
            auto table = static_cast<folder_table_t *>(&self->container);
            table->on_add_share(*self);
        },
        this);
    remove->callback(
        [](auto, void *data) {
            auto self = reinterpret_cast<device_share_widget_t *>(data);
            auto table = static_cast<folder_table_t *>(&self->container);
            self->device = {};
            bool ok = table->on_remove_share(*self, self->device, self->initial_device);
            if (!ok) {
                self->input->value(0);
            }
        },
        this);
    input->callback(
        [](auto, void *data) {
            auto self = reinterpret_cast<device_share_widget_t *>(data);
            auto table = static_cast<folder_table_t *>(&self->container);
            auto previous = self->device;
            if (self->input->value()) {
                auto cluster = table->container.supervisor.get_cluster();
                for (auto &it : cluster->get_devices()) {
                    auto device = it.item.get();
                    if (device == cluster->get_device().get()) {
                        continue;
                    }
                    auto short_id = device->device_id().get_short();
                    auto label = fmt::format("{}, {}", device->get_name(), short_id);
                    if (label == self->input->text()) {
                        self->device = it.item;
                        break;
                    }
                }
            } else {
                self->device = {};
            }
            table->on_select(self->device, previous);
        },
        this);

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

void device_share_widget_t::reset() {
    auto &table = static_cast<folder_table_t &>(this->container);
    auto cluster = table.container.supervisor.get_cluster();

    input->add("(empty)");
    int i = 1;
    int index = i;
    for (auto &it : cluster->get_devices()) {
        auto &device = it.item;
        if (device == cluster->get_device()) {
            continue;
        }
        auto short_id = device->device_id().get_short();
        auto label = fmt::format("{}, {}", device->get_name(), short_id);
        input->add(label.data());
        if (device.get() == initial_device.get()) {
            this->device = device;
            index = i;
        }
        ++i;
    }
    input->value(this->device ? index : 0);
    if (disabled) {
        widget->deactivate();
    }
}

bool device_share_widget_t::store(void *data) {
    if (device) {
        auto ctx = reinterpret_cast<ctx_t *>(data);
        ctx->shared_with.put(device);
    }
    return true;
}

struct checkbox_widget_t : table_widget::checkbox_t {
    using parent_t = table_widget::checkbox_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<folder_table_t *>(data)->refresh(); }, &container);
        return r;
    }
};

} // namespace

auto folder_table_t::make_title(folder_table_t &container, std::string_view title) -> widgetable_ptr_t {
    struct widget_t final : table_widget::label_t {
        using parent_t = table_widget::label_t;

        widget_t(Fl_Widget &container, std::string_view title_) : parent_t(container), title{title_} {}

        void reset() override { input->label(title.data()); }

        std::string_view title;
    };
    return new widget_t(container, title);
}

auto folder_table_t::make_path(folder_table_t &container, bool disabled) -> widgetable_ptr_t {
    struct widget_t final : table_widget::path_t {
        using parent_t = table_widget::path_t;
        widget_t(Fl_Widget &container, std::string title, bool disabled_)
            : parent_t{container, title}, disabled{disabled_} {}

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

        void reset() override {
            auto &container = static_cast<folder_table_t &>(this->container);
            auto &path = container.description.get_folder()->get_path();
            auto value = boost::nowide::narrow(path.wstring());
            input->value(value.data());
        }

        bool store(void *data) override {
            auto ctx = reinterpret_cast<ctx_t *>(data);
            db::set_path(ctx->folder, input->value());
            return true;
        }

        bool disabled;
    };
    return new widget_t(container, "folder directory", disabled);
}

auto folder_table_t::make_id(folder_table_t &container, bool disabled) -> widgetable_ptr_t {
    struct widget_t final : table_widget::input_t {
        using parent_t = table_widget::input_t;
        widget_t(Fl_Widget &container, bool disabled_) : parent_t{container}, disabled{disabled_} {}

        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<folder_table_t *>(data)->refresh(); }, &container);
            input->when(input->when() | FL_WHEN_CHANGED);
            if (disabled) {
                widget->deactivate();
            }
            return r;
        }

        void reset() override {
            auto &container = static_cast<folder_table_t &>(this->container);
            auto value = container.description.get_folder()->get_id();
            input->value(value.data());
        }

        bool store(void *data) override {
            auto ctx = reinterpret_cast<ctx_t *>(data);
            db::set_id(ctx->folder, input->value());
            return true;
        }

        bool disabled;
    };
    return new widget_t(container, disabled);
}

auto folder_table_t::make_label(folder_table_t &container, bool disabled) -> widgetable_ptr_t {
    struct widget_t final : table_widget::input_t {
        using parent_t = table_widget::input_t;
        widget_t(Fl_Widget &container, bool disabled_) : parent_t{container}, disabled{disabled_} {}

        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<folder_table_t *>(data)->refresh(); }, &container);
            input->when(input->when() | FL_WHEN_CHANGED);
            if (disabled) {
                widget->deactivate();
            }
            return r;
        }

        void reset() override {
            auto &container = static_cast<folder_table_t &>(this->container);
            auto value = container.description.get_folder()->get_label();
            input->value(value.data());
        }

        bool store(void *data) override {
            auto label = std::string_view(input->value());
            if (label.empty()) {
                auto &container = static_cast<folder_table_t &>(this->container);
                container.error = "label cannot be empty";
                return false;
            }

            auto ctx = reinterpret_cast<ctx_t *>(data);
            db::set_label(ctx->folder, label);
            return true;
        }

        bool disabled;
    };
    return new widget_t(container, disabled);
}

auto folder_table_t::make_folder_type(folder_table_t &container, bool disabled) -> widgetable_ptr_t {
    struct widget_t final : table_widget::choice_t {
        using parent_t = table_widget::choice_t;
        widget_t(Fl_Widget &container, bool disabled_) : parent_t{container}, disabled{disabled_} {}

        Fl_Widget *create_widget(int x, int y, int w, int h) override {
            auto r = parent_t::create_widget(x, y, w, h);
            input->size(200, r->h());
            input->callback([](auto, void *data) { reinterpret_cast<folder_table_t *>(data)->refresh(); }, &container);
            input->when(input->when() | FL_WHEN_CHANGED);
            input->add("Send only");
            input->add("Receive only");
            input->add("Send and Receive");
            if (disabled) {
                widget->deactivate();
            }
            return r;
        }

        void reset() override {
            auto &container = static_cast<folder_table_t &>(this->container);
            auto value = container.description.get_folder()->get_folder_type();
            input->value(static_cast<int>(value));
        }

        bool store(void *data) override {
            auto ctx = reinterpret_cast<ctx_t *>(data);
            auto value = (db::FolderType)(input->value());

            db::set_folder_type(ctx->folder, value);
            return true;
        }
        bool disabled;
    };
    return new widget_t(container, disabled);
}

auto folder_table_t::make_pull_order(folder_table_t &container, bool disabled) -> widgetable_ptr_t {
    struct widget_t final : table_widget::choice_t {
        using parent_t = table_widget::choice_t;
        widget_t(Fl_Widget &container, bool disabled_) : parent_t{container}, disabled{disabled_} {}

        Fl_Widget *create_widget(int x, int y, int w, int h) override {
            auto r = parent_t::create_widget(x, y, w, h);
            input->size(200, r->h());
            input->callback([](auto, void *data) { reinterpret_cast<folder_table_t *>(data)->refresh(); }, &container);
            input->when(input->when() | FL_WHEN_CHANGED);
            input->add("random");
            input->add("alphabetic");
            input->add("smallest first");
            input->add("largest first");
            input->add("oldest first");
            input->add("newest first");
            if (disabled) {
                widget->deactivate();
            }
            return r;
        }

        void reset() override {
            auto &container = static_cast<folder_table_t &>(this->container);
            auto value = container.description.get_folder()->get_pull_order();
            input->value(static_cast<int>(value));
        }

        bool store(void *data) override {
            auto ctx = reinterpret_cast<ctx_t *>(data);
            auto value = (db::PullOrder)(input->value());

            db::set_pull_order(ctx->folder, value);
            return true;
        }
        bool disabled;
    };
    return new widget_t(container, disabled);
}

auto folder_table_t::make_index(folder_table_t &container, bool disabled) -> widgetable_ptr_t {
    struct widget_t final : table_widget::int_input_t {
        using parent_t = table_widget::int_input_t;
        widget_t(Fl_Widget &container, bool disabled_) : parent_t{container}, disabled{disabled_} {}

        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<folder_table_t *>(data)->refresh(); }, &container);
            input->when(input->when() | FL_WHEN_CHANGED);
            if (disabled) {
                widget->deactivate();
            }
            return r;
        }

        void reset() override {
            auto &container = static_cast<folder_table_t &>(this->container);
            auto value = container.description.get_index();
            auto value_str = fmt::format("0x{:x}", value);
            input->value(value_str.data());
        }

        bool store(void *data) override {
            auto ctx = reinterpret_cast<ctx_t *>(data);
            auto value_str = std::string_view(input->value());
            std::uint64_t value = 0;
            auto b = value_str.begin();
            if (value_str.size() >= 2 && value_str.starts_with("0x")) {
                b += 2;
            }
            auto result = std::from_chars(b, value_str.end(), value, 16);
            if (result.ec != std::errc() || value <= 0) {
                auto &container = static_cast<folder_table_t &>(this->container);
                container.error = "invalid index";
                return false;
            }
            ctx->index = value;
            return true;
        }

        bool disabled;
    };
    return new widget_t(container, disabled);
}

auto folder_table_t::make_rescan_interval(folder_table_t &container, bool disabled) -> widgetable_ptr_t {
    struct widget_t final : table_widget::int_input_t {
        using parent_t = table_widget::int_input_t;
        widget_t(Fl_Widget &container, bool disabled_) : parent_t{container}, disabled{disabled_} {}

        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<folder_table_t *>(data)->refresh(); }, &container);
            input->when(input->when() | FL_WHEN_CHANGED);
            if (disabled) {
                widget->deactivate();
            }
            return r;
        }

        void reset() override {
            auto &container = static_cast<folder_table_t &>(this->container);
            auto value = container.description.get_folder()->get_rescan_interval();
            auto value_str = std::to_string(value);
            input->value(value_str.data());
        }

        bool store(void *data) override {
            auto ctx = reinterpret_cast<ctx_t *>(data);
            auto value_str = std::string_view(input->value());
            int value = 0;
            auto result = std::from_chars(value_str.begin(), value_str.end(), value);
            if (result.ec != std::errc() || value <= 0) {
                auto &container = static_cast<folder_table_t &>(this->container);
                container.error = "invalid rescan interval";
                return false;
            }

            auto ri = static_cast<std::uint32_t>(value);
            db::set_rescan_interval(ctx->folder, ri);
            return true;
        }
        bool disabled;
    };
    return new widget_t(container, disabled);
}

auto folder_table_t::make_ignore_permissions(folder_table_t &container, bool disabled) -> widgetable_ptr_t {
    struct widget_t final : checkbox_widget_t {
        using parent_t = checkbox_widget_t;
        widget_t(Fl_Widget &container, bool disabled_) : parent_t{container}, disabled{disabled_} {}

        Fl_Widget *create_widget(int x, int y, int w, int h) override {
            auto r = parent_t::create_widget(x, y, w, h);
            if (disabled) {
                widget->deactivate();
            }
            return r;
        }
        void reset() override {
            auto &container = static_cast<folder_table_t &>(this->container);
            input->value(container.description.get_folder()->are_permissions_ignored());
        }

        bool store(void *data) override {
            auto ctx = reinterpret_cast<ctx_t *>(data);
            return true;
        }
        bool disabled;
    };
    return new widget_t(container, disabled);
}

auto folder_table_t::make_ignore_delete(folder_table_t &container, bool disabled) -> widgetable_ptr_t {
    struct widget_t final : checkbox_widget_t {
        using parent_t = checkbox_widget_t;
        widget_t(Fl_Widget &container, bool disabled_) : parent_t{container}, disabled{disabled_} {}

        Fl_Widget *create_widget(int x, int y, int w, int h) override {
            auto r = parent_t::create_widget(x, y, w, h);
            if (disabled) {
                widget->deactivate();
            }
            return r;
        }
        void reset() override {
            auto &container = static_cast<folder_table_t &>(this->container);
            input->value(container.description.get_folder()->is_deletion_ignored());
        }
        bool store(void *data) override {
            auto ctx = reinterpret_cast<ctx_t *>(data);
            db::set_ignore_delete(ctx->folder, input->value());
            return true;
        }
        bool disabled;
    };
    return new widget_t(container, disabled);
}

auto folder_table_t::make_disable_tmp(folder_table_t &container, bool disabled) -> widgetable_ptr_t {
    struct widget_t final : checkbox_widget_t {
        using parent_t = checkbox_widget_t;
        widget_t(Fl_Widget &container, bool disabled_) : parent_t{container}, disabled{disabled_} {}

        Fl_Widget *create_widget(int x, int y, int w, int h) override {
            auto r = parent_t::create_widget(x, y, w, h);
            if (disabled) {
                widget->deactivate();
            }
            return r;
        }
        void reset() override {
            auto &container = static_cast<folder_table_t &>(this->container);
            input->value(container.description.get_folder()->are_temp_indixes_disabled());
        }
        bool store(void *data) override {
            auto ctx = reinterpret_cast<ctx_t *>(data);
            db::set_disable_temp_indexes(ctx->folder, input->value());
            return true;
        }
        bool disabled;
    };
    return new widget_t(container, disabled);
}

auto folder_table_t::make_paused(folder_table_t &container, bool disabled) -> widgetable_ptr_t {
    struct widget_t final : checkbox_widget_t {
        using parent_t = checkbox_widget_t;
        widget_t(Fl_Widget &container, bool disabled_) : parent_t{container}, disabled{disabled_} {}

        Fl_Widget *create_widget(int x, int y, int w, int h) override {
            auto r = parent_t::create_widget(x, y, w, h);
            if (disabled) {
                widget->deactivate();
            }
            return r;
        }
        void reset() override {
            auto &container = static_cast<folder_table_t &>(this->container);
            input->value(container.description.get_folder()->is_paused());
        }
        bool store(void *data) override {
            auto ctx = reinterpret_cast<ctx_t *>(data);
            db::set_paused(ctx->folder, input->value());
            return true;
        }
        bool disabled;
    };
    return new widget_t(container, disabled);
}

auto folder_table_t::make_scheduled(folder_table_t &container, bool disabled) -> widgetable_ptr_t {
    struct widget_t final : checkbox_widget_t {
        using parent_t = checkbox_widget_t;
        widget_t(Fl_Widget &container, bool disabled_) : parent_t{container}, disabled{disabled_} {}

        Fl_Widget *create_widget(int x, int y, int w, int h) override {
            auto r = parent_t::create_widget(x, y, w, h);
            if (disabled) {
                widget->deactivate();
            }
            return r;
        }
        void reset() override {
            auto &container = static_cast<folder_table_t &>(this->container);
            input->value(container.description.get_folder()->is_scheduled());
        }
        bool store(void *data) override {
            auto ctx = reinterpret_cast<ctx_t *>(data);
            db::set_scheduled(ctx->folder, input->value());
            return true;
        }
        bool disabled;
    };
    return new widget_t(container, disabled);
}

auto folder_table_t::make_shared_with(folder_table_t &container, model::device_ptr_t device, bool disabled)
    -> widgetable_ptr_t {
    return new device_share_widget_t(container, device, disabled);
}

auto folder_table_t::make_notice(folder_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<folder_table_t &>(container).error;
            input->label(label.c_str());
        }
    };
    return new widget_t(container);
}

folder_table_t::folder_table_t(tree_item_t &container_, const model::folder_info_t &description_, int x, int y, int w,
                               int h)
    : parent_t(x, y, w, h), container{container_}, description{description_}, apply_button{nullptr},
      share_button{nullptr}, reset_button{nullptr} {

    shared_with.reset(new model::devices_map_t{});
    non_shared_with.reset(new model::devices_map_t{});

    auto folder = description.get_folder();
    auto cluster = folder->get_cluster();
    for (auto it : cluster->get_devices()) {
        auto &device = it.item;
        if (device != cluster->get_device()) {
            if (folder->is_shared_with(*device)) {
                shared_with->put(device);
            } else {
                non_shared_with->put(device);
            }
        }
    }
}

bool folder_table_t::on_remove_share(widgetable_t &widget, model::device_ptr_t device, model::device_ptr_t initial) {
    bool removed = false;
    auto [_, count] = scan(widget);
    if (count > 1 && !initial) {
        parent_t::remove_row(widget);
        removed = (bool)device;
    } else {
        redraw();
    }

    if (device) {
        shared_with->remove(device);
        non_shared_with->put(device);
    }

    container.refresh_content();
    return removed;
}

void folder_table_t::on_select(model::device_ptr_t device, model::device_ptr_t previous) {
    if (previous) {
        shared_with->remove(previous);
        non_shared_with->put(previous);
    }
    if (device) {
        shared_with->put(device);
        non_shared_with->remove(device);
    }
    container.refresh_content();
}

void folder_table_t::on_add_share(widgetable_t &widget) {
    auto [from_index, count] = scan(widget);
    auto devices_amount = static_cast<int>(container.supervisor.get_cluster()->get_devices().size());
    if (count < devices_amount - 1) {
        assert(from_index);
        auto w = widgetable_ptr_t{};
        w.reset(new device_share_widget_t(*this, {}, false));
        insert_row("shared with", w, from_index + 1);
        refresh();
    }
}

std::pair<int, int> folder_table_t::scan(widgetable_t &widget) {
    auto &rows = get_rows();
    auto from_index = int{-1};
    int count = 0;
    for (int i = 0; i < static_cast<int>(rows.size()); ++i) {
        auto item = std::get_if<widgetable_ptr_t>(&rows[i].value);
        if (!item) {
            continue;
        }
        if (!dynamic_cast<device_share_widget_t *>(item->get())) {
            continue;
        }
        ++count;
        if (item->get() == &widget) {
            from_index = i;
        }
    }
    return {from_index, count};
}

void folder_table_t::on_share() {
    serialization_context_t ctx;
    auto valid = store(&ctx);
    if (!valid) {
        return;
    }

    auto &folder = ctx.folder;
    auto &sup = container.supervisor;
    auto cluster = sup.get_cluster();
    auto log = sup.get_logger();
    auto &peer = ctx.shared_with.begin()->item;
    auto label = db::get_label(folder);
    auto folder_id = db::get_id(folder);
    auto peer_id = peer->device_id().get_sha256();
    auto existing_folder = sup.get_cluster()->get_folders().by_id(folder_id);
    if (!existing_folder) {
        log->info("going to create folder {}({}) & share it with {}", label, folder_id, peer->get_name());
        auto opt = modify::upsert_folder_t::create(*sup.get_cluster(), sup.get_sequencer(), folder, 0);
        if (!opt) {
            log->error("cannot create folder: {}", opt.assume_error().message());
            return;
        }

        auto cb = sup.call_share_folders(folder_id, {utils::bytes_t(peer_id.begin(), peer_id.end())});
        sup.send_model<model::payload::model_update_t>(opt.assume_value(), cb.get());
    } else {
        log->info("going to share folder {}({}) with {}", label, folder_id, peer->get_name());
        auto &sequncecer = sup.get_sequencer();
        using diff_t = model::diff::modify::share_folder_t;
        auto &self = *cluster->get_device();
        auto opt = diff_t::create(*cluster, sequncecer, *peer, self.device_id(), *existing_folder);
        if (!opt) {
            auto message = opt.assume_error().message();
            log->error("cannot share folder {} with {} : {}", folder_id, peer->device_id(), message);
            return;
        }

        auto diff = std::move(opt.value());
        auto cb = sup.call_select_folder(folder_id);
        sup.send_model<model::payload::model_update_t>(diff, cb.get());
    }
}

void folder_table_t::on_apply() {
    serialization_context_t ctx;
    auto valid = store(&ctx);
    if (!valid) {
        return;
    }

    auto &sup = container.supervisor;
    auto &folder_db = ctx.folder;
    auto log = sup.get_logger();
    auto &cluster = *sup.get_cluster();

    auto opt = modify::upsert_folder_t::create(*sup.get_cluster(), sup.get_sequencer(), folder_db, ctx.index);
    if (!opt) {
        log->error("cannot create folder: {}", opt.assume_error().message());
        return;
    }
    auto &diff = opt.value();
    auto current = diff.get();

    if (initially_shared_with.size()) {
        auto folder = description.get_folder();
        auto orphaned_blocks = model::orphaned_blocks_t{};
        auto &folder_infos = folder->get_folder_infos();
        for (auto it : initially_shared_with) {
            auto &device = it.item;
            if (!ctx.shared_with.by_sha256(device->device_id().get_sha256())) {
                auto folder_info = folder_infos.by_device(*device);
                if (folder_info) {
                    log->info("going to unshare folder '{}' with {}({})", folder->get_label(), device->get_name(),
                              device->device_id().get_short());
                    auto sub_diff = model::diff::cluster_diff_ptr_t{};
                    sub_diff = new modify::unshare_folder_t(cluster, *folder_info, &orphaned_blocks);
                    current = current->assign_sibling(sub_diff.get());
                }
            }
        }
        if (auto orphaned_set = orphaned_blocks.deduce(); orphaned_set.size()) {
            log->info("going to remove {} orphaned blocks", orphaned_set.size());
            auto sub_diff = model::diff::cluster_diff_ptr_t{};
            sub_diff = new modify::remove_blocks_t(std::move(orphaned_set));
            current = current->assign_sibling(sub_diff.get());
        }
    }

    auto devices = std::vector<utils::bytes_t>{};
    for (auto it : initially_non_shared_with) {
        auto &device = it.item;
        auto sha256 = device->device_id().get_sha256();
        if (ctx.shared_with.by_sha256(sha256)) {
            devices.emplace_back(utils::bytes_t(sha256.begin(), sha256.end()));
        }
    }

    auto folder_id = db::get_id(folder_db);
    auto cb =
        devices.empty() ? sup.call_select_folder(folder_id) : sup.call_share_folders(folder_id, std::move(devices));
    sup.send_model<model::payload::model_update_t>(diff, cb.get());
}

void folder_table_t::on_reset() {
    auto &rows = get_rows();
    for (int i = 0; i < static_cast<int>(rows.size()); ++i) {
        auto &row = rows[i];
        auto widget = std::get_if<widgetable_ptr_t>(&row.value);
        if (!widget) {
            continue;
        }
        auto share = dynamic_cast<device_share_widget_t *>(widget->get());
        if (!share) {
            continue;
        }
        bool remove = false;
        auto &device = share->initial_device;
        if (!device) {
            remove = true;
        } else {
            auto device_id = device->device_id().get_sha256();
            remove = !initially_shared_with.by_sha256(device_id);
        }
        if (remove) {
            remove_row(**widget);
            --i;
        }
    }
    reset();
    refresh();
}

void folder_table_t::on_remove() {
    auto r = fl_choice("Are you sure? (no files on disk are touched)", "Yes", "No", nullptr);
    if (r != 0) {
        return;
    }
    auto &sup = container.supervisor;
    auto &cluster = *sup.get_cluster();
    auto &sequencer = sup.get_sequencer();
    auto &folder = *description.get_folder();
    auto diff = model::diff::cluster_diff_ptr_t{};
    diff = new model::diff::modify::suspend_folder_t(folder);

    auto postponed = model::diff::cluster_diff_ptr_t{};
    postponed = new model::diff::modify::remove_folder_t(cluster, sequencer, *description.get_folder());
    sup.send_sink<model::payload::model_update_t>(std::move(diff), postponed.detach());
}

void folder_table_t::on_rescan() {
    auto &sup = container.supervisor;
    auto diff = model::diff::cluster_diff_ptr_t{};
    auto folder_id = description.get_folder()->get_id();
    diff = new model::diff::local::scan_request_t(folder_id);
    sup.send_model<model::payload::model_update_t>(std::move(diff), this);
}


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

#pragma once

#include "../static_table.h"
#include "../tree_item.h"
#include "proto/proto-helpers.h"

#include <boost/smart_ptr/local_shared_ptr.hpp>

namespace syncspirit::fltk::content {

struct folder_table_t : static_table_t {
    using parent_t = static_table_t;
    using shared_devices_t = boost::local_shared_ptr<model::devices_map_t>;

    struct serialization_context_t {
        db::Folder folder;
        std::uint64_t index;
        model::devices_map_t shared_with;
    };

    static widgetable_ptr_t make_title(folder_table_t &container, std::string_view title);
    static widgetable_ptr_t make_path(folder_table_t &container, bool disabled);
    static widgetable_ptr_t make_id(folder_table_t &container, bool disabled);
    static widgetable_ptr_t make_label(folder_table_t &container, bool disabled);
    static widgetable_ptr_t make_folder_type(folder_table_t &container, bool disabled);
    static widgetable_ptr_t make_pull_order(folder_table_t &container, bool disabled);
    static widgetable_ptr_t make_index(folder_table_t &container, bool disabled);
    static widgetable_ptr_t make_rescan_interval(folder_table_t &container, bool disabled);
    static widgetable_ptr_t make_ignore_permissions(folder_table_t &container, bool disabled);
    static widgetable_ptr_t make_ignore_delete(folder_table_t &container, bool disabled);
    static widgetable_ptr_t make_disable_tmp(folder_table_t &container, bool disabled);
    static widgetable_ptr_t make_scheduled(folder_table_t &container, bool disabled);
    static widgetable_ptr_t make_paused(folder_table_t &container, bool disabled);
    static widgetable_ptr_t make_shared_with(folder_table_t &container, model::device_ptr_t device, bool disabled);
    static widgetable_ptr_t make_notice(folder_table_t &container);

    folder_table_t(tree_item_t &container_, const model::folder_info_t &description, int x, int y, int w, int h);

    // void refresh() override;

    bool on_remove_share(widgetable_t &widget, model::device_ptr_t device, model::device_ptr_t initial);
    void on_select(model::device_ptr_t device, model::device_ptr_t previous);
    void on_add_share(widgetable_t &widget);
    std::pair<int, int> scan(widgetable_t &widget);

    void on_remove();
    void on_apply();
    void on_share();
    void on_reset();
    void on_rescan();

    tree_item_t &container;
    const model::folder_info_t &description;
    model::devices_map_t initially_shared_with;
    model::devices_map_t initially_non_shared_with;
    shared_devices_t shared_with;
    shared_devices_t non_shared_with;
    std::string error;
    widgetable_ptr_t notice;
    static_string_provider_ptr_t entries_cell;
    static_string_provider_ptr_t entries_size_cell;
    static_string_provider_ptr_t max_sequence_cell;
    static_string_provider_ptr_t scan_start_cell;
    static_string_provider_ptr_t scan_finish_cell;
    Fl_Widget *apply_button;
    Fl_Widget *share_button;
    Fl_Widget *reset_button;
    Fl_Widget *rescan_button;
};

} // namespace syncspirit::fltk::content


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

#include "log_colors.h"

namespace syncspirit::fltk {

color_array_t log_colors = {
    fl_rgb_color(220, 255, 220), fl_rgb_color(240, 255, 240), // trace
    fl_rgb_color(210, 245, 255), fl_rgb_color(230, 250, 255), // debug
    fl_rgb_color(245, 245, 245), fl_rgb_color(255, 255, 255), // info
    fl_rgb_color(255, 250, 200), fl_rgb_color(255, 250, 220), // warn
    fl_rgb_color(255, 220, 220), fl_rgb_color(255, 240, 240), // error
    fl_rgb_color(255, 220, 255), fl_rgb_color(255, 240, 255)  // critical
};

Fl_Color table_selection_color = FL_GREEN;

} // namespace syncspirit::fltk


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

#pragma once

#include <FL/Enumerations.H>
#include <array>

namespace syncspirit::fltk {

using color_array_t = std::array<Fl_Color, 12>;

extern color_array_t log_colors;
extern Fl_Color table_selection_color;

} // namespace syncspirit::fltk


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

#include "log_panel.h"

#include "log_table.h"
#include "log_colors.h"
#include "utils/io.h"

#include <spdlog/fmt/fmt.h>
#include <FL/Fl_Box.H>
#include <FL/fl_draw.H>
#include <FL/Fl_Native_File_Chooser.H>
#include <boost/algorithm/string/replace.hpp>

using fmt::format_to;

using namespace syncspirit::fltk;

static constexpr int padding = 5;

static std::mutex incoming_mutex;
log_panel_t *widget;

struct syncspirit::fltk::fltk_sink_t final : base_sink_t {
    fltk_sink_t() {}
    fltk_sink_t(const fltk_sink_t &) = delete;
    fltk_sink_t(fltk_sink_t &&) = default;

    void forward(log_record_ptr_t record) override {
        auto lock = std::unique_lock(incoming_mutex);
        if (widget) {
            widget->incoming_records.push_back(std::move(record));
        }
    }
};

static void _pull_in_logs(void *data) {
    reinterpret_cast<log_panel_t *>(data)->pull_in_logs();
    Fl::add_timeout(0.05, _pull_in_logs, data);
}

static void auto_scroll_toggle(Fl_Widget *widget, void *data) {
    auto button = static_cast<Fl_Toggle_Button *>(widget);
    auto log_table = reinterpret_cast<log_table_t *>(data);
    log_table->autoscroll(button->value());
}

static void clear_logs(Fl_Widget *, void *data) {
    auto panel = reinterpret_cast<log_panel_t *>(data);
    panel->displayed_records->clear();
    panel->records->clear();
    panel->log_table->update();
    panel->log_table->redraw();
}

static void set_min_display_level(Fl_Widget *widget, void *data) {
    auto log_panel = static_cast<log_panel_t *>(widget->parent()->parent());
    auto level_ptr = reinterpret_cast<intptr_t>(data);
    auto level = static_cast<spdlog::level::level_enum>(level_ptr);
    log_panel->min_display_level(level);

    auto &level_buttons = log_panel->level_buttons;
    for (auto it = level_buttons.begin(); it != level_buttons.end(); ++it) {
        if (*it != widget) {
            (**it).value(0);
        }
    }
}

static void on_input_filter(Fl_Widget *widget, void *data) {
    auto input = reinterpret_cast<Fl_Input *>(widget);
    auto log_panel = reinterpret_cast<log_panel_t *>(data);
    log_panel->on_filter(input->value());
}

static void export_log(Fl_Widget *, void *data) {
    struct it_t final : logs_iterator_t {
        it_t(log_buffer_t *buff_) : buff{buff_}, i{0} {}

        log_record_t *next() override {
            if (i < buff->size()) {
                return (*buff)[i++].get();
            }
            return {};
        }

        log_buffer_t *buff;
        size_t i;
    };

    Fl_Native_File_Chooser file_chooser;
    file_chooser.title("Save syncspirit log");
    file_chooser.type(Fl_Native_File_Chooser::BROWSE_SAVE_FILE);
    file_chooser.filter("CSV files\t*.csv");

    auto log_panel = reinterpret_cast<log_panel_t *>(data);
    auto it_selected = log_panel->log_table->get_selected();
    if (!it_selected) {
        it_selected.reset(new it_t(log_panel->displayed_records.get()));
    }

    auto r = file_chooser.show();
    auto &log = log_panel->supervisor.get_logger();
    if (r == -1) {
        log->error("cannot chose file: {}", file_chooser.errmsg());
        return;
    } else if (r == 1) { // cancel
        return;
    }

    // write
    auto filename = file_chooser.filename();
    using file_t = syncspirit::utils::fstream_t;
    auto out = file_t(filename, file_t::binary);
    out << "level, date, source, message" << eol;

    auto escape_message = [](const std::string &msg) -> std::string {
        auto copy = boost::replace_all_copy<std::string>(msg, "\"", "\"\"");
        return fmt::format("\"{}\"", copy);
    };

    while (auto record = it_selected->next()) {
        out << record->level << ", " << record->date << ", " << record->source << ", "
            << escape_message(record->message) << eol;
    }
    log->info("logs wrote to {}", filename);
}

struct counter_label_t : Fl_Box {
    using parent_t = Fl_Box;
    using parent_t::parent_t;

    void draw() override {
        draw_box(box(), x(), y(), w(), h(), FL_BACKGROUND_COLOR);
        fl_draw(label(), x(), y(), w(), h(), FL_ALIGN_RIGHT);
    }
};

log_panel_t::log_panel_t(app_supervisor_t &supervisor_, int x, int y, int w, int h)
    : parent_t{x, y, w, h}, supervisor{supervisor_}, records_counter{nullptr} {
    auto &cfg = supervisor.get_app_config().fltk_config;
    records = std::make_unique<log_buffer_t>(cfg.log_records_buffer);
    displayed_records = std::make_unique<log_buffer_t>(cfg.log_records_buffer);

    auto bottom_row = 30;
    auto log_table_h = h - (padding * 2 + bottom_row);
    auto log_table = new log_table_t(displayed_records, padding, padding, w - padding * 2, log_table_h);
    log_table->autoscroll(true);
    this->log_table = log_table;

    auto common_y = log_table->h() + padding * 2;
    auto common_h = bottom_row - padding;

    bool auto_scroll = true;
    control_group = new Fl_Group(0, common_y, log_table->w(), common_h);
    control_group->begin();
    auto autoscroll_button = new Fl_Toggle_Button(padding, common_y, 100, common_h, "auto scroll");
    autoscroll_button->value(auto_scroll);
    autoscroll_button->callback(auto_scroll_toggle, log_table);

    auto button_x = autoscroll_button->x() + autoscroll_button->w() + padding;

    auto clear_button = new Fl_Button(button_x, common_y, 80, common_h, "clear");
    clear_button->callback(clear_logs, this);
    button_x += clear_button->w() + padding;

    auto export_button = new Fl_Button(button_x, common_y, 80, common_h, "export...");
    export_button->callback(export_log, this);
    button_x += export_button->w() + padding;

    auto trace_button = new Fl_Toggle_Button(button_x, common_y, common_h, common_h, " ");
    trace_button->color(log_colors[static_cast<int>(spdlog::level::trace) * 2]);
    trace_button->selection_color(log_colors[static_cast<int>(spdlog::level::trace) * 2 + 1]);
    trace_button->callback(set_min_display_level, (void *)std::intptr_t(spdlog::level::trace));
    trace_button->tooltip("display log records with trace level and above");
    level_buttons[0] = trace_button;
    button_x += trace_button->w() + 1;

    auto debug_button = new Fl_Toggle_Button(button_x, common_y, common_h, common_h, " ");
    debug_button->color(log_colors[static_cast<int>(spdlog::level::debug) * 2]);
    debug_button->selection_color(log_colors[static_cast<int>(spdlog::level::debug) * 2 + 1]);
    debug_button->callback(set_min_display_level, (void *)std::intptr_t(spdlog::level::debug));
    debug_button->tooltip("display log records with debug level and above");
    level_buttons[1] = debug_button;
    button_x += debug_button->w() + 1;

    auto info_button = new Fl_Toggle_Button(button_x, common_y, common_h, common_h, " ");
    info_button->color(log_colors[static_cast<int>(spdlog::level::info) * 2]);
    info_button->selection_color(log_colors[static_cast<int>(spdlog::level::info) * 2 + 1]);
    info_button->callback(set_min_display_level, (void *)std::intptr_t(spdlog::level::info));
    info_button->tooltip("display log records with info level and above");
    level_buttons[2] = info_button;
    button_x += info_button->w() + 1;

    auto warn_button = new Fl_Toggle_Button(button_x, common_y, common_h, common_h, " ");
    warn_button->color(log_colors[static_cast<int>(spdlog::level::warn) * 2]);
    warn_button->selection_color(log_colors[static_cast<int>(spdlog::level::warn) * 2 + 1]);
    warn_button->callback(set_min_display_level, (void *)std::intptr_t(spdlog::level::warn));
    warn_button->tooltip("display log records with warning level and above");
    level_buttons[3] = warn_button;
    button_x += warn_button->w() + 1;

    auto error_button = new Fl_Toggle_Button(button_x, common_y, common_h, common_h, " ");
    error_button->color(log_colors[static_cast<int>(spdlog::level::err) * 2]);
    error_button->selection_color(log_colors[static_cast<int>(spdlog::level::err) * 2 + 1]);
    error_button->callback(set_min_display_level, (void *)std::intptr_t(spdlog::level::err));
    error_button->tooltip("display log records with error level and above");
    level_buttons[4] = error_button;
    button_x += error_button->w() + 1;

    auto critical_button = new Fl_Toggle_Button(button_x, common_y, common_h, common_h, " ");
    critical_button->color(log_colors[static_cast<int>(spdlog::level::critical) * 2]);
    critical_button->selection_color(log_colors[static_cast<int>(spdlog::level::critical) * 2 + 1]);
    critical_button->callback(set_min_display_level, (void *)std::intptr_t(spdlog::level::critical));
    critical_button->tooltip("display log records with critical level");
    level_buttons[5] = critical_button;
    button_x += critical_button->w() + padding;

    auto enable_button = static_cast<std::size_t>(supervisor.get_app_config().fltk_config.level);
    if (enable_button < level_buttons.size()) {
        level_buttons[enable_button]->value(1);
    }

    auto filter_input = new Fl_Input(button_x, common_y, 200, common_h, "");
    filter_input->callback(on_input_filter, this);
    filter_input->when(FL_WHEN_CHANGED);
    button_x += filter_input->w() + padding;

    auto counter = new counter_label_t(padding + control_group->w() - 200, common_y, 200, common_h);
    counter->tooltip("displayed number of records / total number of records");
    records_counter = counter;

    control_group->end();
    control_group->resizable(counter);

    control_group->end();

    end();

    resizable(log_table);

    auto lock = std::unique_lock(incoming_mutex);
    widget = this;
    auto bridge_sink = sink_ptr_t(new fltk_sink_t());

    auto &dist_sink = supervisor.get_dist_sink();
    for (auto sink : dist_sink->sinks()) {
        auto in_memory_sink = dynamic_cast<im_memory_sink_t *>(sink.get());
        if (in_memory_sink) {
            std::lock_guard lock(in_memory_sink->mutex);
            auto &src = in_memory_sink->records;
            std::move(src.begin(), src.end(), std::back_inserter(*records));
            src.clear();
            dist_sink->remove_sink(sink);
            break;
        }
    }
    supervisor.add_sink(bridge_sink);
    update();
}

void log_panel_t::on_loading_done() {
    pull_in_logs();
    Fl::add_timeout(0.05, _pull_in_logs, this);
}

log_panel_t::~log_panel_t() {
    Fl::remove_timeout(_pull_in_logs, this);
    auto lock = std::unique_lock(incoming_mutex);
    widget = nullptr;
}

void log_panel_t::update(log_queue_t new_records) {
    auto display_level = supervisor.get_app_config().fltk_config.level;
    for (auto &r : new_records) {
        bool display_record = (r->level >= display_level);
        if (display_record && !filter.empty()) {
            display_record =
                (r->source.find(filter) != std::string::npos) || (r->message.find(filter) != std::string::npos);
        }
        if (display_record) {
            displayed_records->push_back(r.get());
        }
        records->push_back(r);
    }
    log_table->update();
    update_counter();
}

void log_panel_t::update() {
    displayed_records->clear();

    auto display_level = supervisor.get_app_config().fltk_config.level;
    for (auto &r : *records) {
        bool display_record = (r->level >= display_level);
        if (display_record && !filter.empty()) {
            display_record =
                (r->source.find(filter) != std::string::npos) || (r->message.find(filter) != std::string::npos);
        }
        if (display_record) {
            displayed_records->push_back(r.get());
        }
    }
    log_table->update();
    update_counter();
}

void log_panel_t::update_counter() {
    if (records_counter) {
        char buff[64] = {0};
        format_to(buff, "{}/{}", displayed_records->size(), records->size());
        records_counter->copy_label(buff);
    }
}

void log_panel_t::min_display_level(spdlog::level::level_enum level) {
    supervisor.get_app_config().fltk_config.level = level;
    update();
}

void log_panel_t::on_filter(std::string_view filter_) {
    filter = filter_;
    update();
}

void log_panel_t::set_splash_text(std::string text) {
    supervisor.get_logger()->info(text);
    pull_in_logs();
}

void log_panel_t::pull_in_logs() {
    auto lock = std::unique_lock(incoming_mutex);
    auto &source = incoming_records;
    if (source.size()) {
        auto copy = std::move(incoming_records);
        lock.unlock();
        update(std::move(copy));
    }
}


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

#pragma once

#include "app_supervisor.h"
#include "log_sink.h"
#include "log_table.h"

#include <FL/Fl_Group.H>
#include <FL/Fl_Toggle_Button.H>
#include <FL/Fl_Input.H>
#include <FL/Fl_Box.H>
#include <array>

namespace syncspirit::fltk {

struct log_panel_t : Fl_Group {
    using parent_t = Fl_Group;
    using sink_ptr_t = spdlog::sink_ptr;
    using level_buttons_t = std::array<Fl_Toggle_Button *, 6>;

    log_panel_t(app_supervisor_t &supervisor, int x, int y, int w, int h);
    ~log_panel_t();

    void min_display_level(spdlog::level::level_enum level);
    void set_display_level(spdlog::level::level_enum level);
    void update();
    void update(log_queue_t new_records);
    void update_counter();
    void pull_in_logs();
    void on_filter(std::string_view filter);
    void set_splash_text(std::string text);
    void on_loading_done();

    app_supervisor_t &supervisor;
    Fl_Group *control_group;
    log_table_t *log_table;
    Fl_Box *records_counter;
    level_buttons_t level_buttons;
    log_queue_t incoming_records;
    log_buffer_ptr_t records;
    log_buffer_ptr_t displayed_records;
    std::string filter;
};

} // namespace syncspirit::fltk


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

#include "log_sink.h"
#include <ctype.h>

using namespace syncspirit::fltk;

base_sink_t::base_sink_t() : date_formatter("%H:%M:%S.%F") {}

void base_sink_t::flush() {}

void base_sink_t::set_pattern(const std::string &) {}

void base_sink_t::set_formatter(std::unique_ptr<spdlog::formatter>) {}

void base_sink_t::log(const spdlog::details::log_msg &msg) {
    spdlog::memory_buf_t formatted;
    date_formatter.format(msg, formatted);
    auto eob = formatted.end() - 1;
    while (!std::isdigit(*eob) && eob > formatted.begin()) {
        --eob;
    }
    ++eob;
    auto date = std::string(formatted.begin(), eob);
    auto source = [&]() { return std::string(msg.logger_name.begin(), msg.logger_name.end()); }();
    auto message = msg.payload;
    auto message_view = std::string_view(message.begin(), message.end());
    auto thread_id = std::to_string(msg.thread_id);

    std::string::size_type pos = 0, prev = 0;
    int index = 0;
    while ((pos = message_view.find("\n", prev)) != std::string::npos) {
        auto sub_message = message_view.substr(prev, pos - prev);
        auto record = [&]() -> log_record_ptr_t {
            if (!index) {
                ++index;
                return new log_record_t(msg.level, std::move(date), std::move(source), std::string(sub_message),
                                        std::move(thread_id));
            } else {
                return new log_record_t(msg.level, "", "", std::string(sub_message), "");
            }
        }();
        prev = pos + 1;
        forward(std::move(record));
    }
    auto sub_message = message_view.substr(prev);
    auto record =
        new log_record_t(msg.level, std::move(date), std::move(source), std::string(sub_message), std::move(thread_id));
    forward(std::move(record));
}

void im_memory_sink_t::forward(log_record_ptr_t record) {
    std::lock_guard guard(mutex);
    records.emplace_back(std::move(record));
}


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

#pragma once

#include "utils/log.h"
#include "log_utils.h"
#include <spdlog/pattern_formatter.h>
#include <spdlog/sinks/sink.h>

namespace syncspirit::fltk {

struct base_sink_t : spdlog::sinks::sink {
    base_sink_t();

    void log(const spdlog::details::log_msg &msg) override;

    void flush() override;
    void set_pattern(const std::string &) override;
    void set_formatter(std::unique_ptr<spdlog::formatter>) override;
    virtual void forward(log_record_ptr_t) = 0;

  private:
    spdlog::pattern_formatter date_formatter;
};

struct im_memory_sink_t final : base_sink_t {
    using base_sink_t::base_sink_t;
    using mutex_t = std::mutex;

    void forward(log_record_ptr_t) override;

    mutex_t mutex;
    log_queue_t records;
};

} // namespace syncspirit::fltk