#include <git2.h>
#include <GLFW/glfw3.h>
#include <imgui.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_opengl3.h>
#include <izo/Clipboard.hpp>
#include <izo/Dialogs.hpp>
#include <izo/Paths.hpp>
#include <IconsTabler.h>
#include "ui/gitree_ui.h"
#include "managers/application_manager.h"
#include "managers/application_icon_cache.h"
#include "managers/avatar_cache.h"
#include "managers/git_manager.h"
#include "managers/user_data.h"
#include "managers/window_manager.h"
#include "models/repository.h"
#include "ui/diff_viewer.h"
#include "ui/graph_renderer.h"

#include <algorithm>
#include <array>
#include <chrono>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <future>
#include <limits>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>

namespace {

std::vector<std::unique_ptr<RepositoryView>> g_tabs;
size_t g_active_tab = 0;
RepositoryView* g_tab_to_select = nullptr;
UserData* g_user_data = nullptr;
AvatarCache* g_avatar_cache = nullptr;
std::array<char, 256> g_filter{};
std::array<char, 128> g_branch_filter{};
std::string g_notice;
std::string g_last_footer_notice;
std::chrono::steady_clock::time_point g_last_footer_notice_change{};
bool g_init_popup = false;
bool g_clone_popup = false;
bool g_about_popup = false;
bool g_licenses_popup = false;
bool g_tag_create_popup = false;
bool g_branch_create_popup = false;
bool g_remote_add_popup = false;
bool g_worktree_add_popup = false;
bool g_submodule_add_popup = false;
bool g_discard_all_popup = false;
bool g_git_auth_popup = false;
std::array<char, 256> g_git_name{};
std::array<char, 1024> g_git_value{};
std::array<char, 1024> g_git_path{};
std::array<char, 256> g_git_auth_username{};
std::array<char, 256> g_git_auth_password{};
std::array<char, 256> g_repository_filter{};
std::array<char, 128> g_create_repository_name{};
std::array<char, 1024> g_create_repository_parent{};
std::array<char, 128> g_create_repository_branch{};
std::array<char, 1024> g_clone_repository_parent{};
std::array<char, 1024> g_clone_repository_url{};
std::string g_git_target;
std::string g_git_target_label;
std::array<char, 256> g_inline_branch_name{};
std::string g_inline_branch_target;
std::string g_requested_branch_checkout;
std::string g_running_branch_checkout;
std::string g_merge_branch_source;
std::string g_merge_branch_target;
bool g_merge_branch_popup = false;
std::string g_pending_commit_jump;
std::string g_git_auth_remote_url;
std::string g_git_auth_error;
bool g_git_auth_remember = true;
RepositoryView* g_expanded_refs_repository = nullptr;
int g_expanded_refs_commit = -1;
RepositoryView* g_inline_branch_repository = nullptr;
int g_inline_branch_commit = -1;
bool g_focus_inline_branch = false;
enum class FileViewMode { path, tree };
FileViewMode g_file_view_mode = FileViewMode::path;
bool g_view_all_files = false;
bool g_file_sort_ascending = true;
bool g_request_branch_selector = false;
std::array<char, 512> g_commit_summary{};
std::array<char, 1024> g_commit_description{};
std::array<char, 256> g_commit_search{};
std::string g_previous_commit_search;
RepositoryView* g_commit_search_repository = nullptr;
int g_commit_search_match = -1;
bool g_commit_search_open = false;
bool g_focus_commit_search = false;
float g_ui_scale = 1.0f;
int g_zoom_percent = 100;
bool g_zoom_reload_requested = false;
bool g_reset_repository_view = false;
float g_sidebar_width = 230.0f;
bool g_sidebar_collapsed = false;
bool g_sidebar_collapsed_before_viewer = false;
bool g_sidebar_auto_collapsed = false;
bool g_previous_code_viewer_open = false;
float g_details_width = 368.0f;
float g_commit_message_height = 125.0f;
float g_working_composer_height = 320.0f;
DiffViewer g_diff_viewer;
WindowManager* g_window_manager = nullptr;
GitManager* g_git_manager = nullptr;
ApplicationManager* g_application_manager = nullptr;
ApplicationIconCache* g_application_icon_cache = nullptr;
ExternalApplicationId g_open_in_application = ExternalApplicationId::visual_studio_code;
ImFont* g_outline_icon_font = nullptr;
ImFont* g_regular_font = nullptr;
ImFont* g_bold_font = nullptr;
ImFont* g_code_font = nullptr;
float g_outline_icon_size = 15.0f;

enum class ToolbarActionRequest { none, pull, push };
ToolbarActionRequest g_pending_toolbar_action = ToolbarActionRequest::none;
ToolbarActionRequest g_running_toolbar_action = ToolbarActionRequest::none;
using RefreshClock = std::chrono::steady_clock;
constexpr auto active_refresh_interval = std::chrono::seconds(2);
constexpr auto background_refresh_interval = std::chrono::seconds(5);

enum class GitAsyncOperation { reload, capture, pull, push, checkout_branch, push_branch, fetch, stash, pop_stash };

struct GitAsyncRequest {
    GitAsyncOperation operation = GitAsyncOperation::reload;
    RepositoryView* tab = nullptr;
    std::string repo_path;
    std::vector<std::string> arguments;
    std::string branch;
    std::string remote;
    std::string success_notice;
    std::string focus_commit;
    std::string preserve_selected_hash;
    std::optional<GitAuthOverride> auth_override;
    bool used_saved_app_credential = false;
    int pull_mode = 1;
    bool surface_errors = false;
};

struct GitAsyncResult {
    GitAsyncOperation operation = GitAsyncOperation::reload;
    RepositoryView* tab = nullptr;
    std::string repo_path;
    bool success = false;
    std::string notice;
    std::string focus_commit;
    std::string preserve_selected_hash;
    GitAsyncRequest retry_request;
    bool auth_required = false;
    bool surface_errors = false;
    std::unique_ptr<RepositoryView> snapshot;
};

std::optional<GitAsyncRequest> g_git_auth_retry_request;

std::future<GitAsyncResult> g_git_async_future;

enum class ToastKind { info, success, warning, error };

struct ToastNotification {
    int id = 0;
    ToastKind kind = ToastKind::info;
    std::string title;
    std::string message;
    float remaining = 5.0f;
    bool hovered = false;
};

std::vector<ToastNotification> g_toasts;
int g_next_toast_id = 1;
std::string g_last_toast_notice;


float ui(float value) { return value * g_ui_scale; }
RepositoryView& repo() { return *g_tabs.at(g_active_tab); }
constexpr float sidebar_child_indent = 24.0f;
constexpr float sidebar_nested_indent = 18.0f;
ImVec4 change_color(FileChangeKind kind);

std::optional<size_t> sidebar_section_index(std::string_view id) {
    if (id == ICON_TB_DESKTOP "  LOCAL") return 0;
    if (id == ICON_TB_CLOUD "  REMOTE") return 1;
    if (id == ICON_TB_TREE "  WORKTREES") return 2;
    if (id == ICON_TB_FOLDER_TREE "  SUBMODULES") return 3;
    return std::nullopt;
}

float combined_ui_scale(float dpi_scale, int zoom_percent) {
    const float zoom_scale = static_cast<float>(zoom_percent) / 100.0f;
    return std::clamp(dpi_scale + zoom_scale - 1.0f, 0.80f, 4.0f);
}

bool text_height_checkbox(const char* label, bool* value) {
    const ImVec2 padding = ImGui::GetStyle().FramePadding;
    ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {padding.x, 0.0f});
    ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, ui(1.0f));
    ImGui::PushStyleColor(ImGuiCol_Border, IM_COL32(70, 76, 87, 255));
    const bool changed = ImGui::Checkbox(label, value);
    ImGui::PopStyleColor();
    ImGui::PopStyleVar();
    ImGui::PopStyleVar();
    return changed;
}

class ScopedUiScale {
public:
    explicit ScopedUiScale(float factor) : previous_scale_(g_ui_scale) {
        g_ui_scale *= factor;
        ImGui::PushFont(nullptr, ImGui::GetStyle().FontSizeBase * factor);
    }
    ~ScopedUiScale() {
        ImGui::PopFont();
        g_ui_scale = previous_scale_;
    }

    ScopedUiScale(const ScopedUiScale&) = delete;
    ScopedUiScale& operator=(const ScopedUiScale&) = delete;

private:
    float previous_scale_;
};

std::string oid_string(const git_oid& oid) {
    char value[GIT_OID_SHA1_HEXSIZE + 1]{};
    git_oid_tostr(value, sizeof(value), &oid);
    return value;
}

bool copy_to_clipboard(std::string_view text, const char* description) {
    std::string error;
    if (!izo::SetClipboardText(text, &error)) {
        g_notice = error.empty() ? "Unable to copy to the clipboard" : error;
        return false;
    }
    g_notice = std::string("Copied ") + description;
    return true;
}

bool contains_case_insensitive_text(std::string_view text, std::string_view needle) {
    return std::search(text.begin(), text.end(), needle.begin(), needle.end(),
        [](unsigned char left, unsigned char right) {
            return std::tolower(left) == std::tolower(right);
        }) != text.end();
}

ToastKind infer_toast_kind(std::string_view message) {
    if (contains_case_insensitive_text(message, "forbidden") ||
            contains_case_insensitive_text(message, "unable") ||
            contains_case_insensitive_text(message, "error") ||
            contains_case_insensitive_text(message, "failed") ||
            contains_case_insensitive_text(message, "denied") ||
            contains_case_insensitive_text(message, "cannot") ||
            contains_case_insensitive_text(message, "no remote") ||
            contains_case_insensitive_text(message, "unsupported"))
        return ToastKind::error;
    if (contains_case_insensitive_text(message, "warning") ||
            contains_case_insensitive_text(message, "already running") ||
            contains_case_insensitive_text(message, "not available"))
        return ToastKind::warning;
    if (contains_case_insensitive_text(message, "complete") ||
            contains_case_insensitive_text(message, "created") ||
            contains_case_insensitive_text(message, "updated") ||
            contains_case_insensitive_text(message, "copied") ||
            contains_case_insensitive_text(message, "opened") ||
            contains_case_insensitive_text(message, "staged") ||
            contains_case_insensitive_text(message, "unstaged") ||
            contains_case_insensitive_text(message, "merged") ||
            contains_case_insensitive_text(message, "applied") ||
            contains_case_insensitive_text(message, "discarded"))
        return ToastKind::success;
    return ToastKind::info;
}

std::string toast_title(ToastKind kind) {
    switch (kind) {
    case ToastKind::success: return "Success";
    case ToastKind::warning: return "Warning";
    case ToastKind::error: return "Error";
    default: return "Notice";
    }
}

void push_toast(std::string message, ToastKind kind = ToastKind::info) {
    if (message.empty()) return;
    ToastNotification toast;
    toast.id = g_next_toast_id++;
    toast.kind = kind;
    toast.title = toast_title(kind);
    toast.message = std::move(message);
    toast.remaining = kind == ToastKind::error ? 9.0f : (kind == ToastKind::warning ? 7.0f : 5.0f);
    g_toasts.push_back(std::move(toast));
    if (g_toasts.size() > 6) g_toasts.erase(g_toasts.begin(), g_toasts.end() - 6);
}

void sync_notice_toast() {
    if (g_notice.empty()) {
        g_last_toast_notice.clear();
        return;
    }
    if (g_notice == g_last_toast_notice) return;
    g_last_toast_notice = g_notice;
    if (g_notice.ends_with("...")) return;
    push_toast(g_notice, infer_toast_kind(g_notice));
}

float footer_notice_alpha() {
    if (g_notice.empty()) {
        g_last_footer_notice.clear();
        return 0.0f;
    }
    if (g_notice != g_last_footer_notice) {
        g_last_footer_notice = g_notice;
        g_last_footer_notice_change = RefreshClock::now();
    }
    if (g_notice.ends_with("...")) return 1.0f;

    constexpr float notice_hold_seconds = 2.0f;
    constexpr float notice_fade_seconds = 1.5f;
    const float elapsed = std::chrono::duration<float>(
        RefreshClock::now() - g_last_footer_notice_change).count();
    if (elapsed <= notice_hold_seconds) return 1.0f;
    if (elapsed >= notice_hold_seconds + notice_fade_seconds) return 0.0f;
    return 1.0f - ((elapsed - notice_hold_seconds) / notice_fade_seconds);
}

void transfer_repository_state(RepositoryView& source, RepositoryView& target) {
    if (&source == &target) return;
    target.close();
    target.repo = source.repo;
    source.repo = nullptr;
    target.commit_walk = source.commit_walk;
    source.commit_walk = nullptr;
    target.history_exhausted = source.history_exhausted;
    target.credentials_checked = source.credentials_checked;
    target.path = std::move(source.path);
    target.name = std::move(source.name);
    target.branch = std::move(source.branch);
    target.local_branches = std::move(source.local_branches);
    target.remote_branches = std::move(source.remote_branches);
    target.local_branch_divergence = std::move(source.local_branch_divergence);
    target.remote_branch_divergence = std::move(source.remote_branch_divergence);
    target.remotes = std::move(source.remotes);
    target.worktrees = std::move(source.worktrees);
    target.worktree_branches = std::move(source.worktree_branches);
    target.submodules = std::move(source.submodules);
    target.submodule_statuses = std::move(source.submodule_statuses);
    target.commits = std::move(source.commits);
    target.working_files = std::move(source.working_files);
    target.selected_commit = source.selected_commit;
    target.scroll_to_commit = source.scroll_to_commit;
    target.last_background_refresh = source.last_background_refresh;
    target.pending_background_refresh = source.pending_background_refresh;
    target.undo_action = std::move(source.undo_action);
    target.redo_action = std::move(source.redo_action);
}

std::string selected_commit_hash(const RepositoryView& repository) {
    if (repository.selected_commit < 0 || repository.selected_commit >= static_cast<int>(repository.commits.size()))
        return {};
    return oid_string(repository.commits[static_cast<size_t>(repository.selected_commit)].oid);
}

void restore_selected_commit(RepositoryView& repository, const std::string& hash) {
    if (hash.empty()) return;
    git_oid target{};
    if (git_oid_fromstr(&target, hash.c_str()) != 0) return;
    for (size_t index = 0; index < repository.commits.size(); ++index) {
        if (git_oid_equal(&repository.commits[index].oid, &target) != 0) {
            repository.selected_commit = static_cast<int>(index);
            return;
        }
    }
}

RepositoryView* find_repository_tab(RepositoryView* tab) {
    const auto iterator = std::find_if(g_tabs.begin(), g_tabs.end(), [tab](const auto& item) {
        return item.get() == tab;
    });
    return iterator == g_tabs.end() ? nullptr : iterator->get();
}

void mark_repository_refreshed(RepositoryView& repository) {
    repository.last_background_refresh = RefreshClock::now();
    repository.pending_background_refresh = false;
}

std::string preferred_auth_remote_name(const RepositoryView& repository, const std::string& remote_hint = {}) {
    if (!remote_hint.empty()) return remote_hint;
    const auto origin = std::find(repository.remotes.begin(), repository.remotes.end(), "origin");
    if (origin != repository.remotes.end()) return *origin;
    return repository.remotes.empty() ? std::string{} : repository.remotes.front();
}

void attach_saved_auth_if_available(RepositoryView& repository, GitAsyncRequest& request) {
    if (!g_user_data || !g_git_manager) return;
    const std::string remote_name = preferred_auth_remote_name(repository, request.remote);
    if (remote_name.empty()) return;
    std::string remote_error;
    const std::string remote_url = g_git_manager->remoteUrl(repository, remote_name, remote_error);
    if (remote_url.empty()) return;
    const auto saved = g_user_data->remoteCredential(remote_url);
    if (!saved) return;
    request.auth_override = GitAuthOverride{remote_url, saved->username, saved->password};
    request.used_saved_app_credential = true;
}

void attach_auth_remote_metadata(RepositoryView& repository, GitAsyncRequest& request) {
    if (!g_git_manager) return;
    const std::string remote_name = preferred_auth_remote_name(repository, request.remote);
    if (remote_name.empty()) return;
    std::string remote_error;
    const std::string remote_url = g_git_manager->remoteUrl(repository, remote_name, remote_error);
    if (remote_url.empty()) return;
    if (request.auth_override) request.auth_override->remote_url = remote_url;
    else request.auth_override = GitAuthOverride{remote_url, {}, {}};
}

GitAsyncResult execute_git_async_request(const GitAsyncRequest& request) {
    GitAsyncResult result;
    result.operation = request.operation;
    result.tab = request.tab;
    result.repo_path = request.repo_path;
    result.focus_commit = request.focus_commit;
    result.preserve_selected_hash = request.preserve_selected_hash;
    result.surface_errors = request.surface_errors;
    result.retry_request = request;

    GitManager manager;
    RepositoryView repository;
    if (!manager.openRepositoryHandle(repository, request.repo_path, result.notice)) return result;

    bool action_ok = true;
    switch (request.operation) {
    case GitAsyncOperation::reload:
        action_ok = manager.reload(repository, result.notice);
        break;
    case GitAsyncOperation::capture: {
        std::string output;
        action_ok = manager.captureGit(repository, request.arguments, output, result.notice) &&
            manager.reload(repository, result.notice);
        if (action_ok) result.notice = request.success_notice;
        break;
    }
    case GitAsyncOperation::pull:
        action_ok = manager.pull(repository, request.pull_mode, result.notice, request.auth_override);
        break;
    case GitAsyncOperation::push:
        action_ok = manager.push(repository, result.notice, request.auth_override);
        break;
    case GitAsyncOperation::checkout_branch:
        action_ok = manager.checkoutBranch(repository, request.branch, result.notice);
        break;
    case GitAsyncOperation::push_branch:
        action_ok = manager.pushBranch(repository, request.branch, result.notice, request.auth_override);
        break;
    case GitAsyncOperation::fetch:
        action_ok = manager.fetch(repository, request.remote, result.notice, request.auth_override);
        break;
    case GitAsyncOperation::stash:
        action_ok = manager.stash(repository, result.notice);
        break;
    case GitAsyncOperation::pop_stash:
        action_ok = manager.popStash(repository, result.notice);
        break;
    }
    if (!action_ok) {
        const bool auth_operation = request.operation == GitAsyncOperation::fetch ||
            request.operation == GitAsyncOperation::pull ||
            request.operation == GitAsyncOperation::push ||
            request.operation == GitAsyncOperation::push_branch;
        if (auth_operation && GitManager::isAuthenticationFailure(result.notice))
            result.auth_required = true;
        return result;
    }

    auto snapshot = std::make_unique<RepositoryView>();
    transfer_repository_state(repository, *snapshot);
    result.snapshot = std::move(snapshot);
    result.success = true;
    return result;
}

void apply_git_async_result(GitAsyncResult result) {
    if (result.operation == GitAsyncOperation::pull || result.operation == GitAsyncOperation::push)
        g_running_toolbar_action = ToolbarActionRequest::none;
    if (result.operation == GitAsyncOperation::checkout_branch)
        g_running_branch_checkout.clear();
    if (result.auth_required) {
        g_git_auth_retry_request = result.retry_request;
        g_git_auth_error = result.notice;
        g_git_auth_remote_url = result.retry_request.auth_override
            ? result.retry_request.auth_override->remote_url
            : g_git_auth_remote_url;
        if (g_user_data && result.retry_request.used_saved_app_credential && !g_git_auth_remote_url.empty())
            g_user_data->clearRemoteCredential(g_git_auth_remote_url);
        if (result.retry_request.auth_override) {
            std::snprintf(g_git_auth_username.data(), g_git_auth_username.size(), "%s",
                result.retry_request.auth_override->username.c_str());
            std::fill(g_git_auth_password.begin(), g_git_auth_password.end(), '\0');
        } else {
            std::fill(g_git_auth_username.begin(), g_git_auth_username.end(), '\0');
            std::fill(g_git_auth_password.begin(), g_git_auth_password.end(), '\0');
        }
        g_git_auth_popup = true;
        return;
    }
    RepositoryView* target = find_repository_tab(result.tab);
    if (!target || target->path != result.repo_path) {
        if ((result.success || result.surface_errors) && !result.notice.empty()) g_notice = result.notice;
        return;
    }
    if (result.success && result.snapshot) {
        transfer_repository_state(*result.snapshot, *target);
        if (!result.focus_commit.empty()) {
            if (target == &repo()) g_pending_commit_jump = result.focus_commit;
            else restore_selected_commit(*target, result.focus_commit);
        } else {
            restore_selected_commit(*target, result.preserve_selected_hash);
        }
        mark_repository_refreshed(*target);
    }
    if ((result.success || result.surface_errors) && !result.notice.empty()) g_notice = result.notice;
}

bool git_async_busy() {
    return g_git_async_future.valid() &&
        g_git_async_future.wait_for(std::chrono::seconds(0)) != std::future_status::ready;
}

void poll_git_async_result() {
    if (!g_git_async_future.valid()) return;
    if (g_git_async_future.wait_for(std::chrono::seconds(0)) != std::future_status::ready) return;
    apply_git_async_result(g_git_async_future.get());
}

bool start_git_async_request(GitAsyncRequest request, const char* busy_notice = "Git operation already running") {
    poll_git_async_result();
    if (git_async_busy()) {
        if (busy_notice && *busy_notice) g_notice = busy_notice;
        return false;
    }
    if (!request.tab || request.repo_path.empty()) return false;
    g_git_async_future = std::async(std::launch::async, [request = std::move(request)]() {
        return execute_git_async_request(request);
    });
    return true;
}

bool reload_repository_background(RepositoryView& repository, bool surface_errors = false) {
    if (!g_git_manager || !repository.repo || repository.path.empty()) return false;
    GitAsyncRequest request;
    request.operation = GitAsyncOperation::reload;
    request.tab = &repository;
    request.repo_path = repository.path;
    request.surface_errors = surface_errors;
    request.preserve_selected_hash = selected_commit_hash(repository);
    const bool started = start_git_async_request(std::move(request), surface_errors
        ? "A Git operation is already running"
        : "");
    if (started) {
        repository.pending_background_refresh = false;
        repository.last_background_refresh = RefreshClock::now();
    }
    return started;
}

void process_repository_background_refreshes() {
    poll_git_async_result();
    if (!g_git_manager || g_tabs.empty()) return;
    if (git_async_busy()) return;
    if (g_running_toolbar_action != ToolbarActionRequest::none) return;
    if (!g_running_branch_checkout.empty() || !g_requested_branch_checkout.empty()) return;
    const RefreshClock::time_point now = RefreshClock::now();
    for (size_t index = 0; index < g_tabs.size(); ++index) {
        RepositoryView& repository = *g_tabs[index];
        if (!repository.repo || repository.path.empty()) continue;
        const auto interval = index == g_active_tab ? active_refresh_interval : background_refresh_interval;
        const bool due = repository.pending_background_refresh ||
            repository.last_background_refresh.time_since_epoch().count() == 0 ||
            now - repository.last_background_refresh >= interval;
        if (due) {
            reload_repository_background(repository, index == g_active_tab);
            break;
        }
    }
}

bool run_repository_action(const std::vector<std::string>& arguments, const std::string& success_notice,
        const std::string& focus_commit = {}) {
    GitAsyncRequest request;
    request.operation = GitAsyncOperation::capture;
    request.tab = &repo();
    request.repo_path = repo().path;
    request.arguments = arguments;
    request.success_notice = success_notice;
    request.focus_commit = focus_commit;
    request.preserve_selected_hash = selected_commit_hash(repo());
    return start_git_async_request(std::move(request));
}

void mark_action_refresh(RepositoryView& repository) {
    mark_repository_refreshed(repository);
    repository.pending_background_refresh = true;
}

void draw_toasts() {
    sync_notice_toast();
    if (g_toasts.empty()) return;

    const float margin = ui(14.0f);
    const float width = ui(340.0f);
    float y = ImGui::GetMainViewport()->WorkPos.y + margin;
    const float right = ImGui::GetMainViewport()->WorkPos.x + ImGui::GetMainViewport()->WorkSize.x - margin;
    const float delta = ImGui::GetIO().DeltaTime;

    for (size_t index = 0; index < g_toasts.size();) {
        ToastNotification& toast = g_toasts[index];
        toast.hovered = false;

        ImGui::SetNextWindowPos({right - width, y}, ImGuiCond_Always);
        ImGui::SetNextWindowSize({width, 0.0f}, ImGuiCond_Always);
        ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, ui(7.0f));
        ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {ui(10.0f), ui(9.0f)});
        ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {ui(8.0f), ui(6.0f)});
        const ImU32 accent_u32 = toast.kind == ToastKind::success ? IM_COL32(74, 201, 126, 255) :
            toast.kind == ToastKind::warning ? IM_COL32(234, 179, 8, 255) :
            toast.kind == ToastKind::error ? IM_COL32(239, 68, 68, 255) :
            IM_COL32(82, 151, 245, 255);
        const ImVec4 accent = ImGui::ColorConvertU32ToFloat4(accent_u32);
        ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.11f, 0.12f, 0.15f, 0.96f));
        ImGui::PushStyleColor(ImGuiCol_Border, accent);
        ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(accent.x, accent.y, accent.z, 0.12f));
        ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(accent.x, accent.y, accent.z, 0.24f));
        ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(accent.x, accent.y, accent.z, 0.30f));

        const std::string window_name = "##toast_" + std::to_string(toast.id);
        ImGui::Begin(window_name.c_str(), nullptr,
            ImGuiWindowFlags_NoDecoration |
            ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings |
            ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoFocusOnAppearing);

        const ImVec2 min = ImGui::GetWindowPos();
        const ImVec2 max = {min.x + ImGui::GetWindowWidth(), min.y + ImGui::GetWindowHeight()};
        ImGui::GetWindowDrawList()->AddRectFilled(min, {min.x + ui(4.0f), max.y}, accent_u32, ui(7.0f),
            ImDrawFlags_RoundCornersLeft);

        ImGui::PushFont(g_bold_font, 0.0f);
        ImGui::TextColored(accent, "%s", toast.title.c_str());
        ImGui::PopFont();
        ImGui::SameLine();
        ImGui::SetCursorPosX(ImGui::GetWindowWidth() - ui(70.0f));
        if (ImGui::SmallButton((ICON_TB_COPY "##toast_copy_" + std::to_string(toast.id)).c_str()))
            copy_to_clipboard(toast.message, "notification");
        ImGui::SameLine();
        bool dismiss = ImGui::SmallButton((ICON_TB_XMARK "##toast_close_" + std::to_string(toast.id)).c_str());

        ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + width - ui(26.0f));
        ImGui::TextUnformatted(toast.message.c_str());
        ImGui::PopTextWrapPos();

        const float progress = std::clamp(toast.remaining /
            (toast.kind == ToastKind::error ? 9.0f : (toast.kind == ToastKind::warning ? 7.0f : 5.0f)), 0.0f, 1.0f);
        const ImVec2 progress_min = {min.x + ui(10.0f), max.y - ui(5.0f)};
        const ImVec2 progress_max = {max.x - ui(10.0f), max.y - ui(3.0f)};
        ImGui::GetWindowDrawList()->AddRectFilled(progress_min, progress_max, IM_COL32(255, 255, 255, 18), ui(2.0f));
        ImGui::GetWindowDrawList()->AddRectFilled(progress_min,
            {progress_min.x + (progress_max.x - progress_min.x) * progress, progress_max.y}, accent_u32, ui(2.0f));

        toast.hovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
        if (!toast.hovered) toast.remaining -= delta;
        y += ImGui::GetWindowHeight() + ui(8.0f);

        ImGui::End();
        ImGui::PopStyleColor(5);
        ImGui::PopStyleVar(3);

        if (dismiss || toast.remaining <= 0.0f) {
            g_toasts.erase(g_toasts.begin() + static_cast<std::ptrdiff_t>(index));
            continue;
        }
        ++index;
    }
}

bool start_branch_checkout_async(RepositoryView& repository, const std::string& branch) {
    GitAsyncRequest request;
    request.operation = GitAsyncOperation::checkout_branch;
    request.tab = &repository;
    request.repo_path = repository.path;
    request.branch = branch;
    return start_git_async_request(std::move(request));
}

bool start_push_branch_async(RepositoryView& repository, const std::string& branch) {
    GitAsyncRequest request;
    request.operation = GitAsyncOperation::push_branch;
    request.tab = &repository;
    request.repo_path = repository.path;
    request.branch = branch;
    request.preserve_selected_hash = selected_commit_hash(repository);
    attach_auth_remote_metadata(repository, request);
    attach_saved_auth_if_available(repository, request);
    return start_git_async_request(std::move(request));
}

bool start_fetch_async(RepositoryView& repository, const std::string& remote, bool surface_errors = true) {
    GitAsyncRequest request;
    request.operation = GitAsyncOperation::fetch;
    request.tab = &repository;
    request.repo_path = repository.path;
    request.remote = remote;
    request.surface_errors = surface_errors;
    request.preserve_selected_hash = selected_commit_hash(repository);
    attach_auth_remote_metadata(repository, request);
    attach_saved_auth_if_available(repository, request);
    return start_git_async_request(std::move(request), "A Git operation is already running");
}

bool start_toolbar_action_async(RepositoryView& repository, ToolbarActionRequest action) {
    GitAsyncRequest request;
    request.tab = &repository;
    request.repo_path = repository.path;
    request.preserve_selected_hash = selected_commit_hash(repository);
    if (action == ToolbarActionRequest::pull) {
        request.operation = GitAsyncOperation::pull;
        request.pull_mode = g_user_data->pullMode();
    } else if (action == ToolbarActionRequest::push) {
        request.operation = GitAsyncOperation::push;
    } else {
        return false;
    }
    attach_auth_remote_metadata(repository, request);
    attach_saved_auth_if_available(repository, request);
    return start_git_async_request(std::move(request));
}

bool start_stash_async(RepositoryView& repository, bool pop) {
    GitAsyncRequest request;
    request.operation = pop ? GitAsyncOperation::pop_stash : GitAsyncOperation::stash;
    request.tab = &repository;
    request.repo_path = repository.path;
    request.preserve_selected_hash = selected_commit_hash(repository);
    return start_git_async_request(std::move(request));
}

void draw_dotted_bezier(ImDrawList* draw, const ImVec2& start, const ImVec2& control_start,
        const ImVec2& control_end, const ImVec2& end, ImU32 color, float radius, float spacing) {
    const float estimated_length = std::hypot(end.x - start.x, end.y - start.y) +
        std::hypot(control_start.x - start.x, control_start.y - start.y) +
        std::hypot(end.x - control_end.x, end.y - control_end.y);
    const int dots = std::max(2, static_cast<int>(estimated_length / spacing));
    for (int index = 0; index <= dots; ++index) {
        const float t = static_cast<float>(index) / static_cast<float>(dots);
        const float inverse = 1.0f - t;
        const float a = inverse * inverse * inverse;
        const float b = 3.0f * inverse * inverse * t;
        const float c = 3.0f * inverse * t * t;
        const float d = t * t * t;
        draw->AddCircleFilled({
            a * start.x + b * control_start.x + c * control_end.x + d * end.x,
            a * start.y + b * control_start.y + c * control_end.y + d * end.y,
        }, radius, color);
    }
}

void draw_copyable_hash(std::string_view visible, std::string_view full_hash) {
    ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.31f, 0.76f, 0.92f, 1.0f));
    ImGui::TextUnformatted(visible.data(), visible.data() + visible.size());
    ImGui::PopStyleColor();
    const bool hovered = ImGui::IsItemHovered();
    const bool clicked = ImGui::IsItemClicked();
    if (hovered) {
        ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
        ImGui::SetTooltip("Copy: %.*s", static_cast<int>(full_hash.size()), full_hash.data());
    }
    if (clicked) copy_to_clipboard(full_hash, "commit hash");
}

void draw_jumpable_hash(std::string_view visible, std::string_view full_hash) {
    ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.31f, 0.76f, 0.92f, 1.0f));
    ImGui::TextUnformatted(visible.data(), visible.data() + visible.size());
    ImGui::PopStyleColor();
    if (ImGui::IsItemHovered()) {
        ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
        ImGui::SetTooltip("Jump to commit in graph");
    }
    if (ImGui::IsItemClicked()) g_pending_commit_jump.assign(full_hash);
}

void cancel_inline_branch() {
    g_inline_branch_repository = nullptr;
    g_inline_branch_commit = -1;
    g_inline_branch_target.clear();
    g_inline_branch_name.fill('\0');
    g_focus_inline_branch = false;
}

void cancel_merge_branch() {
    g_merge_branch_source.clear();
    g_merge_branch_target.clear();
    g_merge_branch_popup = false;
}

void begin_inline_branch(int commit_index) {
    if (commit_index < 0 || commit_index >= static_cast<int>(repo().commits.size())) {
        g_notice = "Create an initial commit before creating a branch";
        return;
    }
    char target[GIT_OID_SHA1_HEXSIZE + 1]{};
    git_oid_tostr(target, sizeof(target), &repo().commits[static_cast<size_t>(commit_index)].oid);
    g_inline_branch_repository = &repo();
    g_inline_branch_commit = commit_index;
    g_inline_branch_target = target;
    g_inline_branch_name.fill('\0');
    g_focus_inline_branch = true;
}

void persist_repository_session() {
    if (!g_user_data || g_tabs.empty()) return;
    std::vector<std::string> paths;
    paths.reserve(g_tabs.size());
    for (const auto& tab : g_tabs) paths.push_back(tab->path);
    g_user_data->setRepositorySession(std::move(paths), g_active_tab);
}

void clear_sidebar_filter() {
    g_filter.fill('\0');
}

void request_branch_merge(const std::string& source_branch, const std::string& target_branch) {
    if (source_branch.empty() || target_branch.empty() || source_branch == target_branch) return;
    g_merge_branch_source = source_branch;
    g_merge_branch_target = target_branch;
    g_merge_branch_popup = true;
}

void request_branch_checkout(const std::string& branch) {
    g_requested_branch_checkout = branch;
}

void reset_repository_view() {
    if (g_tabs.empty()) return;
    RepositoryView& view = repo();
    view.selected_commit = 0;
    view.scroll_to_commit = view.commits.empty() ? -1 : 0;
    g_diff_viewer.close();
    g_commit_search_open = false;
    g_commit_search_repository = nullptr;
    g_commit_search.fill('\0');
    g_previous_commit_search.clear();
    g_commit_search_match = -1;
    g_filter.fill('\0');
    g_branch_filter.fill('\0');
    g_view_all_files = false;
    g_file_view_mode = FileViewMode::path;
    g_expanded_refs_repository = nullptr;
    g_expanded_refs_commit = -1;
    cancel_inline_branch();
    cancel_merge_branch();
    g_requested_branch_checkout.clear();
    g_running_branch_checkout.clear();
    g_reset_repository_view = true;
}

void activate_repository_tab(size_t index) {
    if (index >= g_tabs.size() || index == g_active_tab) return;
    g_active_tab = index;
    g_tab_to_select = g_tabs[index].get();
    reset_repository_view();
    g_tabs[index]->pending_background_refresh = true;
    persist_repository_session();
}

void create_new_tab(bool persist = true) {
    g_tabs.push_back(std::make_unique<RepositoryView>());
    g_active_tab = g_tabs.size() - 1;
    g_tab_to_select = g_tabs.back().get();
    reset_repository_view();
    if (persist) persist_repository_session();
}

void close_tab(size_t index) {
    if (index >= g_tabs.size()) return;
    const bool closing_active_tab = index == g_active_tab;
    if (g_inline_branch_repository == g_tabs[index].get()) cancel_inline_branch();
    cancel_merge_branch();
    if (g_user_data && !g_tabs[index]->path.empty()) g_user_data->addRecentlyClosed(g_tabs[index]->path);
    g_tabs.erase(g_tabs.begin() + static_cast<std::ptrdiff_t>(index));
    if (g_tabs.empty()) create_new_tab();
    else if (g_active_tab >= g_tabs.size()) g_active_tab = g_tabs.size() - 1;
    else if (index < g_active_tab) --g_active_tab;
    if (closing_active_tab && !g_tabs.empty()) reset_repository_view();
    persist_repository_session();
}

bool open_repository(const char* path) {
    const bool opened = g_git_manager->openRepository(repo(), path, g_notice);
    if (opened) {
        mark_repository_refreshed(repo());
        if (g_user_data && path && *path) g_user_data->addRecentRepository(path);
        reset_repository_view();
        persist_repository_session();
    }
    return opened;
}

void pick_and_open_repository() {
    izo::DialogOptions options;
    options.title = "Open Git repository";
    if (!repo().path.empty()) options.initialPath = repo().path;
    else options.initialPath = izo::GetKnownPath(izo::KnownPath::CurrentDirectory);

    const izo::DialogResult result = izo::PickFolder(options);
    if (result.status == izo::DialogStatus::Cancelled) return;
    if (result.status == izo::DialogStatus::Error) {
        g_notice = result.errorMessage.empty() ? "Unable to open the folder picker" : result.errorMessage;
        return;
    }
    if (result.paths.empty()) {
        g_notice = "The folder picker returned no path";
        return;
    }

    const auto utf8 = result.paths.front().u8string();
    const std::string path(reinterpret_cast<const char*>(utf8.data()), utf8.size());
    if (repo().repo) create_new_tab();
    open_repository(path.c_str());
}

bool init_repository(const char* path, const char* initial_branch = "main") {
    const bool initialized = g_git_manager->initializeRepository(repo(), path, initial_branch, g_notice);
    if (initialized) {
        if (g_user_data && path && *path) g_user_data->addRecentRepository(path);
        persist_repository_session();
    }
    return initialized;
}

std::filesystem::path default_repository_parent() {
    std::filesystem::path documents = izo::GetKnownPath(izo::KnownPath::Documents);
    const std::filesystem::path github = documents / "GitHub";
    std::error_code error;
    return std::filesystem::is_directory(github, error) ? github : documents;
}

template <size_t Size>
void set_path_buffer(std::array<char, Size>& buffer, const std::filesystem::path& path) {
    const auto utf8 = path.u8string();
    const std::string value(reinterpret_cast<const char*>(utf8.data()), utf8.size());
    std::snprintf(buffer.data(), buffer.size(), "%s", value.c_str());
}

bool pick_folder_into(std::array<char, 1024>& buffer, const char* title) {
    izo::DialogOptions options;
    options.title = title;
    options.initialPath = buffer[0] ? std::filesystem::path(buffer.data()) : default_repository_parent();
    const izo::DialogResult result = izo::PickFolder(options);
    if (result.status == izo::DialogStatus::Cancelled) return false;
    if (result.status == izo::DialogStatus::Error || result.paths.empty()) {
        g_notice = result.errorMessage.empty() ? "Unable to select a folder" : result.errorMessage;
        return false;
    }
    set_path_buffer(buffer, result.paths.front());
    return true;
}

void apply_style(float scale) {
    g_ui_scale = scale;
    ImGuiStyle& style = ImGui::GetStyle();
    style = ImGuiStyle();
    style.WindowRounding = 0.0f;
    style.WindowBorderSize = 0.0f;
    style.ChildRounding = 0.0f;
    style.ChildBorderSize = 0.0f;
    style.FrameRounding = 4.0f;
    style.FrameBorderSize = 0.0f;
    style.PopupRounding = 6.0f;
    style.PopupBorderSize = 0.0f;
    style.ScrollbarRounding = 6.0f;
    style.GrabRounding = 0.0f;
    style.TabRounding = 6.0f;
    style.TabBorderSize = 0.0f;
    style.TabBarBorderSize = 0.0f;
    style.TabBarOverlineSize = 0.0f;
    style.WindowPadding = {4.0f, 4.0f};
    style.FramePadding = {6.0f, 3.0f};
    style.CellPadding = {6.0f, 2.0f};
    style.ItemSpacing = {6.0f, 4.0f};
    style.ItemInnerSpacing = {5.0f, 3.0f};
    style.IndentSpacing = 16.0f;
    style.ScrollbarSize = 10.0f;
    style.GrabMinSize = 8.0f;
    const auto color = [](int red, int green, int blue, int alpha = 255) {
        return ImVec4(red / 255.0f, green / 255.0f, blue / 255.0f, alpha / 255.0f);
    };
    style.Colors[ImGuiCol_Text] = color(218, 221, 226);
    style.Colors[ImGuiCol_TextDisabled] = color(154, 159, 168);
    style.Colors[ImGuiCol_WindowBg] = color(22, 24, 29);
    style.Colors[ImGuiCol_MenuBarBg] = color(25, 27, 32);
    style.Colors[ImGuiCol_ChildBg] = color(22, 24, 29);
    style.Colors[ImGuiCol_PopupBg] = color(34, 37, 44);
    style.Colors[ImGuiCol_Border] = color(55, 59, 67);
    style.Colors[ImGuiCol_BorderShadow] = color(0, 0, 0, 0);
    style.Colors[ImGuiCol_FrameBg] = color(31, 34, 40);
    style.Colors[ImGuiCol_FrameBgHovered] = color(43, 47, 55);
    style.Colors[ImGuiCol_FrameBgActive] = color(48, 52, 61);
    style.Colors[ImGuiCol_Button] = color(42, 45, 52);
    style.Colors[ImGuiCol_ButtonHovered] = color(51, 55, 63);
    style.Colors[ImGuiCol_ButtonActive] = color(58, 63, 72);
    style.Colors[ImGuiCol_Header] = color(37, 51, 79);
    style.Colors[ImGuiCol_HeaderHovered] = color(45, 58, 84);
    style.Colors[ImGuiCol_HeaderActive] = color(49, 64, 91);
    style.Colors[ImGuiCol_Tab] = color(43, 47, 56);
    style.Colors[ImGuiCol_TabHovered] = color(51, 56, 66);
    style.Colors[ImGuiCol_TabSelected] = color(58, 63, 74);
    style.Colors[ImGuiCol_TabDimmed] = color(43, 47, 56);
    style.Colors[ImGuiCol_TabDimmedSelected] = color(53, 58, 68);
    style.Colors[ImGuiCol_Separator] = color(55, 59, 67, 100);
    style.Colors[ImGuiCol_SeparatorHovered] = color(23, 181, 204);
    style.Colors[ImGuiCol_SeparatorActive] = color(23, 181, 204);
    style.Colors[ImGuiCol_TableHeaderBg] = color(30, 33, 39);
    style.Colors[ImGuiCol_TableRowBg] = color(22, 24, 29);
    style.Colors[ImGuiCol_TableRowBgAlt] = color(22, 24, 29);
    style.Colors[ImGuiCol_CheckMark] = color(23, 181, 204);
    style.Colors[ImGuiCol_ScrollbarBg] = color(0, 0, 0, 0);
    style.Colors[ImGuiCol_ScrollbarGrab] = color(67, 71, 80);
    style.Colors[ImGuiCol_ScrollbarGrabHovered] = color(82, 87, 97);
    style.Colors[ImGuiCol_NavHighlight] = color(23, 181, 204);
    style.ScaleAllSizes(scale);
}

void load_fonts(float scale) {
    ImGuiIO& io = ImGui::GetIO();
    io.Fonts->Clear();
    g_regular_font = nullptr;
    g_bold_font = nullptr;
    g_code_font = nullptr;
    g_outline_icon_font = nullptr;
    ImFontConfig config;
    config.OversampleH = 2;
    config.OversampleV = 2;
    config.PixelSnapH = false;
    config.RasterizerDensity = 1.0f;
    const float size = 18.0f * scale;
    g_regular_font = io.Fonts->AddFontFromFileTTF(GITREE_ASSET_DIR "/Inter-Regular.ttf", size, &config);
    if (!g_regular_font) g_regular_font = io.Fonts->AddFontDefault();
    io.FontDefault = g_regular_font;

    static constexpr ImWchar icon_ranges[] = {ICON_MIN_TB, ICON_MAX_TB, 0};
    const float icon_size = 14.0f * scale;
    const auto merge_icons = [&] {
        ImFontConfig icon_config;
        icon_config.MergeMode = true;
        icon_config.PixelSnapH = true;
        icon_config.GlyphMinAdvanceX = icon_size;
        icon_config.GlyphOffset.y = 1.0f * scale;
        io.Fonts->AddFontFromFileTTF(
            GITREE_ASSET_DIR "/tabler-icons-outline.ttf", icon_size, &icon_config, icon_ranges);
    };
    merge_icons();

    ImFontConfig bold_config = config;
    g_bold_font = io.Fonts->AddFontFromFileTTF(
        GITREE_ASSET_DIR "/Inter-SemiBold.ttf", size, &bold_config);
    if (!g_bold_font) g_bold_font = g_regular_font;
    else merge_icons();

    ImFontConfig code_config = config;
    g_code_font = io.Fonts->AddFontFromFileTTF(
        "C:/Windows/Fonts/CascadiaMono.ttf", size, &code_config);
    if (!g_code_font)
        g_code_font = io.Fonts->AddFontFromFileTTF("C:/Windows/Fonts/CascadiaCode.ttf", size, &code_config);
    if (!g_code_font) g_code_font = g_regular_font;
    else merge_icons();

    static constexpr ImWchar outline_ranges[] = {
        0xEA5E, 0xEA5E,
        0xEA76, 0xEA76,
        0xEB64, 0xEB64,
        0xEF86, 0xEF86,
        0,
    };
    ImFontConfig outline_config;
    outline_config.PixelSnapH = true;
    g_outline_icon_size = size;
    g_outline_icon_font = io.Fonts->AddFontFromFileTTF(
        GITREE_ASSET_DIR "/tabler-icons-outline.ttf", size, &outline_config, outline_ranges);
    apply_style(scale);
}

bool sidebar_collapse_row(const char* id, const std::string& label, bool default_open,
        float reserved_width, bool show_chevron = true, bool bold_text = false) {
    ImGui::PushID(id);
    const ImGuiID state_id = ImGui::GetID("collapse_state");
    ImGuiStorage* storage = ImGui::GetStateStorage();
    if (const auto section = sidebar_section_index(id); section && g_user_data)
        default_open = g_user_data->sidebarSectionOpen(*section);
    bool open = storage->GetBool(state_id, default_open);
    const ImVec2 size{
        std::max(ui(24.0f), ImGui::GetContentRegionAvail().x - reserved_width),
        ui(24.0f),
    };
    const bool clicked = ImGui::InvisibleButton("##collapse", size);
    const ImVec2 minimum = ImGui::GetItemRectMin();
    ImDrawList* draw = ImGui::GetWindowDrawList();
    const bool hovered = ImGui::IsItemHovered();
    constexpr ImU32 icon_color = IM_COL32(112, 118, 128, 255);
    const ImU32 chevron_color = hovered
        ? IM_COL32(218, 223, 231, 255) : IM_COL32(126, 132, 142, 255);
    const ImU32 text_color = hovered
        ? IM_COL32(238, 241, 246, 255) : IM_COL32(190, 195, 204, 255);
    const float y = minimum.y + (size.y - ImGui::GetFontSize()) * 0.5f;
    const float icon_size = ImGui::GetFontSize() * 0.90f;
    const float icon_y = minimum.y + (size.y - icon_size) * 0.5f;
    if (show_chevron)
        draw->AddText(g_regular_font, icon_size, {minimum.x + ui(3.0f), icon_y}, chevron_color,
            open ? ICON_TB_CHEVRON_DOWN : ICON_TB_CHEVRON_RIGHT);
    const size_t divider = label.find("  ");
    if (divider != std::string::npos) {
        const std::string icon = label.substr(0, divider);
        const std::string text = label.substr(divider + 2);
        const char* rendered_icon = !show_chevron && open && icon == ICON_TB_FOLDER
            ? ICON_TB_FOLDER_OPEN : icon.c_str();
        const float icon_x = show_chevron ? 20.0f : 3.0f;
        const float text_x = show_chevron ? 39.0f : 23.0f;
        draw->AddText(g_regular_font, icon_size, {minimum.x + ui(icon_x), icon_y},
            icon_color, rendered_icon);
        draw->AddText(bold_text ? g_bold_font : g_regular_font, ImGui::GetFontSize(),
            {minimum.x + ui(text_x), y}, text_color, text.c_str());
    } else {
        draw->AddText(bold_text ? g_bold_font : g_regular_font, ImGui::GetFontSize(),
            {minimum.x + ui(show_chevron ? 20.0f : 3.0f), y}, text_color, label.c_str());
    }
    if (clicked) {
        open = !open;
        storage->SetBool(state_id, open);
        if (const auto section = sidebar_section_index(id); section && g_user_data)
            g_user_data->setSidebarSectionOpen(*section, open);
    }
    ImGui::PopID();
    return open;
}

bool sidebar_item_row(const char* icon, const std::string& text, const std::string& id,
        const char* status_icon = nullptr, ImU32 status_color = 0,
        ImU32 item_icon_color = IM_COL32(112, 118, 128, 255),
        const std::string& trailing_text = {}, bool active_branch = false) {
    ImGui::PushID(id.c_str());
    const bool clicked = ImGui::InvisibleButton("##sidebar_item", {-1, ui(24.0f)});
    const ImVec2 minimum = ImGui::GetItemRectMin();
    const ImVec2 maximum = ImGui::GetItemRectMax();
    ImDrawList* draw = ImGui::GetWindowDrawList();
    const bool hovered = ImGui::IsItemHovered();
    if (active_branch) {
        const ImVec2 fill_min{ImGui::GetWindowPos().x, minimum.y};
        draw->AddRectFilled(fill_min, maximum,
            hovered ? IM_COL32(69, 122, 85, 240) : IM_COL32(56, 103, 72, 228), ui(3.0f));
    }
    else if (hovered)
        draw->AddRectFilled(minimum, maximum, IM_COL32(51, 55, 63, 210));
    const float y = minimum.y + (maximum.y - minimum.y - ImGui::GetFontSize()) * 0.5f;
    const float icon_size = ImGui::GetFontSize() * 0.90f;
    const float icon_y = minimum.y + (maximum.y - minimum.y - icon_size) * 0.5f;
    const float trailing_width = trailing_text.empty() ? 0.0f : ImGui::CalcTextSize(trailing_text.c_str()).x;
    const float trailing_x = maximum.x - ui(4.0f) - trailing_width;
    const float content_right = trailing_text.empty() ? maximum.x : trailing_x - ui(7.0f);
    draw->PushClipRect(minimum, {std::max(minimum.x, content_right), maximum.y}, true);
    const float content_offset = status_icon ? ui(20.0f) : 0.0f;
    if (status_icon)
        draw->AddText(g_regular_font, icon_size, {minimum.x + ui(3.0f), icon_y},
            status_color, status_icon);
    draw->AddText(g_regular_font, icon_size,
        {minimum.x + ui(3.0f) + content_offset, icon_y}, item_icon_color, icon);
    draw->AddText({minimum.x + ui(23.0f) + content_offset, y},
        IM_COL32(205, 209, 216, 255), text.c_str());
    draw->PopClipRect();
    if (!trailing_text.empty()) {
        draw->PushClipRect(minimum, maximum, true);
        draw->AddText({trailing_x, y}, IM_COL32(163, 169, 180, 255), trailing_text.c_str());
        draw->PopClipRect();
    }
    ImGui::PopID();
    return clicked;
}

bool sidebar_section_is_open(const char* id) {
    ImGui::PushID(id);
    bool default_open = true;
    if (const auto section = sidebar_section_index(id); section && g_user_data)
        default_open = g_user_data->sidebarSectionOpen(*section);
    const bool open = ImGui::GetStateStorage()->GetBool(ImGui::GetID("collapse_state"), default_open);
    ImGui::PopID();
    return open;
}

bool sidebar_section_header(const char* label, int count, const char* add_tooltip, const char* add_notice) {
    const ImVec2 header_min = ImGui::GetCursorScreenPos();
    const float header_width = ImGui::GetContentRegionAvail().x;
    const bool open = sidebar_collapse_row(label, label, true, ui(62.0f), true, true);
    const ImVec2 mouse = ImGui::GetIO().MousePos;
    const bool header_hovered = ImGui::IsWindowHovered() && mouse.x >= header_min.x &&
        mouse.x <= header_min.x + header_width && mouse.y >= header_min.y &&
        mouse.y <= header_min.y + ui(24.0f);

    char count_text[16]{};
    std::snprintf(count_text, sizeof(count_text), "%d", count);
    const ImVec2 count_size = ImGui::CalcTextSize(count_text);
    const float button_size = ui(24.0f);
    const float button_x = header_min.x + header_width - button_size - ui(4.0f);
    const float text_y = header_min.y + (ui(24.0f) - count_size.y) * 0.5f;
    ImGui::GetWindowDrawList()->AddText(
        {button_x - ui(8.0f) - count_size.x, text_y}, ImGui::GetColorU32(ImGuiCol_TextDisabled), count_text);

    if (header_hovered) {
        ImGui::PushID(label);
        ImGui::SetCursorScreenPos({button_x, header_min.y});
        const bool clicked = ImGui::InvisibleButton("##add", {button_size, button_size});
        const ImVec2 button_min = ImGui::GetItemRectMin();
        const ImVec2 button_max = ImGui::GetItemRectMax();
        ImDrawList* draw = ImGui::GetWindowDrawList();
        draw->AddRectFilled(button_min, button_max,
            ImGui::IsItemHovered() ? IM_COL32(35, 72, 48, 255) : IM_COL32(31, 51, 39, 255));
        draw->AddRect(button_min, button_max, IM_COL32(82, 184, 103, 255), 0.0f, 0, ui(1.0f));
        const ImVec2 center{(button_min.x + button_max.x) * 0.5f, (button_min.y + button_max.y) * 0.5f};
        draw->AddLine({center.x - ui(3.5f), center.y}, {center.x + ui(3.5f), center.y},
            IM_COL32(151, 218, 164, 255), ui(1.0f));
        draw->AddLine({center.x, center.y - ui(3.5f)}, {center.x, center.y + ui(3.5f)},
            IM_COL32(151, 218, 164, 255), ui(1.0f));
        if (clicked) {
            const std::string section = label;
            if (section.find("LOCAL") != std::string::npos) {
                if (repo().commits.empty()) g_notice = "Create an initial commit before creating a branch";
                else {
                    g_git_target = repo().branch;
                    g_git_target_label = repo().branch.empty() ? "HEAD" : repo().branch;
                    g_branch_create_popup = true;
                }
            }
            else if (section.find("REMOTE") != std::string::npos) g_remote_add_popup = true;
            else if (section.find("WORKTREES") != std::string::npos) g_worktree_add_popup = true;
            else if (section.find("SUBMODULES") != std::string::npos) g_submodule_add_popup = true;
            else g_notice = add_notice;
        }
        if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) ImGui::SetTooltip("%s", add_tooltip);
        ImGui::PopID();
    }
    ImGui::SetCursorScreenPos({header_min.x, header_min.y + ui(24.0f)});
    return open;
}

void sidebar_section_splitter(const char* id, size_t section_index, float current_height,
        float next_height, float minimum_height, size_t next_section_index, bool persist_next_height) {
    ImGui::PushID(id);
    ImGui::InvisibleButton("##vertical_resize", {-1, ui(5.0f)});
    const bool active = ImGui::IsItemActive();
    const bool hovered = ImGui::IsItemHovered();
    if (active || hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS);
    if (active) {
        const float total_height = current_height + next_height;
        const float height = std::clamp(current_height + ImGui::GetIO().MouseDelta.y / g_ui_scale,
            minimum_height, std::max(minimum_height, total_height - minimum_height));
        g_user_data->setSidebarSectionHeight(section_index, height);
        if (persist_next_height) g_user_data->setSidebarSectionHeight(next_section_index, total_height - height);
    }
    if (ImGui::IsItemDeactivated()) g_user_data->save();
    const ImVec2 minimum = ImGui::GetItemRectMin();
    const ImVec2 maximum = ImGui::GetItemRectMax();
    ImGui::GetWindowDrawList()->AddLine(
        {minimum.x, (minimum.y + maximum.y) * 0.5f},
        {maximum.x, (minimum.y + maximum.y) * 0.5f},
        active || hovered ? IM_COL32(23, 181, 204, 255) : IM_COL32(55, 59, 67, 255));
    ImGui::PopID();
}

enum class SidebarItemKind { worktree, submodule };

void sidebar_item_context(const std::string& item, SidebarItemKind kind) {
    if (!ImGui::BeginPopupContextItem()) return;
    if (kind == SidebarItemKind::submodule && ImGui::MenuItem(ICON_TB_DOWNLOAD "  Pull / Update"))
        g_git_manager->updateSubmodule(repo(), item, g_notice);
    if (ImGui::MenuItem(ICON_TB_FOLDER_OPEN "  Open in file manager")) {
        std::string error;
        const std::filesystem::path path = kind == SidebarItemKind::worktree
            ? g_git_manager->worktreePath(repo(), item, error)
            : std::filesystem::path(repo().path) / item;
        if (path.empty() || !izo::OpenPath(path, &error)) g_notice = error;
    }
    ImGui::Separator();
    if (ImGui::MenuItem(ICON_TB_COPY "  Copy name")) copy_to_clipboard(item, "name");
    ImGui::EndPopup();
}

void section(const char* label, const std::vector<std::string>& items, const char* item_icon,
        const char* add_tooltip, const char* add_notice, SidebarItemKind kind, size_t section_index,
        float body_height, float next_height, float minimum_height, size_t next_section_index,
        bool persist_next_height, bool resizable) {
    const bool open = sidebar_section_header(label, static_cast<int>(items.size()), add_tooltip, add_notice);
    if (open && body_height >= ui(1.0f)) {
        if (g_reset_repository_view) ImGui::SetNextWindowScroll({0.0f, 0.0f});
        ImGui::BeginChild((std::string(label) + "##body").c_str(), {-1, body_height});
        ImGui::Indent(ui(sidebar_child_indent));
        const std::vector<std::string> snapshot = items;
        for (const auto& item : snapshot) {
            if (g_filter[0] && item.find(g_filter.data()) == std::string::npos) continue;
            if (kind == SidebarItemKind::submodule) {
                const auto found = repo().submodule_statuses.find(item);
                const unsigned int status = found == repo().submodule_statuses.end() ? 0 : found->second;
                const bool in_sync = found != repo().submodule_statuses.end() &&
                    (status & GIT_SUBMODULE_STATUS_IN_HEAD) != 0 &&
                    (status & GIT_SUBMODULE_STATUS_IN_WD) != 0 &&
                    GIT_SUBMODULE_STATUS_IS_UNMODIFIED(status);
                sidebar_item_row(item_icon, item, item,
                    in_sync ? ICON_TB_CHECK : ICON_TB_TRIANGLE_EXCLAMATION,
                    in_sync ? IM_COL32(70, 201, 101, 255) : IM_COL32(230, 165, 72, 255));
                if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) {
                    if (in_sync)
                        ImGui::SetTooltip("This submodule is in-sync with your repository (%s)",
                            repo().name.c_str());
                    else if ((status & GIT_SUBMODULE_STATUS_WD_UNINITIALIZED) != 0)
                        ImGui::SetTooltip("This submodule is not initialized (%s)", item.c_str());
                    else if (GIT_SUBMODULE_STATUS_IS_WD_DIRTY(status))
                        ImGui::SetTooltip("This submodule has local changes (%s)", item.c_str());
                    else
                        ImGui::SetTooltip("This submodule is out-of-sync with your repository (%s)",
                            repo().name.c_str());
                }
            } else {
                sidebar_item_row(item_icon, item, item);
            }
            sidebar_item_context(item, kind);
        }
        ImGui::Unindent(ui(sidebar_child_indent));
        ImGui::EndChild();
        if (resizable)
            sidebar_section_splitter(label, section_index, body_height / g_ui_scale, next_height, minimum_height,
                next_section_index, persist_next_height);
    } else {
        ImGui::Separator();
    }
}

struct BranchNode {
    std::map<std::string, BranchNode> children;
    std::string full_name;
    bool branch = false;
};

void add_branch_node(BranchNode& root, const std::string& branch) {
    BranchNode* node = &root;
    size_t start = 0;
    while (start < branch.size()) {
        const size_t slash = branch.find('/', start);
        const std::string part = branch.substr(start, slash == std::string::npos ? std::string::npos : slash - start);
        node = &node->children[part];
        if (slash == std::string::npos) break;
        start = slash + 1;
    }
    node->branch = true;
    node->full_name = branch;
}

void branch_context(const std::string& branch, bool remote) {
    if (!ImGui::BeginPopupContextItem()) return;
    if (!remote && ImGui::MenuItem(ICON_TB_CODE_BRANCH "  Checkout")) {
        request_branch_checkout(branch);
        clear_sidebar_filter();
    }
    if (!remote && ImGui::MenuItem(ICON_TB_ARROW_RIGHT_ARROW_LEFT "  Merge into current branch")) {
        request_branch_merge(branch, repo().branch);
        clear_sidebar_filter();
    }
    if (!remote && ImGui::MenuItem(ICON_TB_UPLOAD "  Push branch")) {
        start_push_branch_async(repo(), branch);
        clear_sidebar_filter();
    }
    if (remote && ImGui::MenuItem(ICON_TB_DOWNLOAD "  Fetch")) {
        const size_t slash = branch.find('/');
        start_fetch_async(repo(), slash == std::string::npos ? branch : branch.substr(0, slash));
    }
    ImGui::Separator();
    if (ImGui::MenuItem(ICON_TB_COPY "  Copy branch name")) copy_to_clipboard(branch, "branch name");
    ImGui::EndPopup();
}

void branch_drag_source(const std::string& branch, bool remote) {
    if (!ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) return;
    ImGui::SetDragDropPayload("GITREE_BRANCH", branch.c_str(), branch.size() + 1);
    ImGui::TextUnformatted(remote ? ICON_TB_CLOUD "  Remote branch" : ICON_TB_CODE_BRANCH "  Local branch");
    ImGui::TextDisabled("%s", branch.c_str());
    ImGui::EndDragDropSource();
}

void branch_drag_target(const std::string& branch) {
    if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) &&
            ImGui::GetDragDropPayload() != nullptr) {
        ImGui::SetTooltip("%s Drop to merge into %s", ICON_TB_ARROW_RIGHT_ARROW_LEFT, branch.c_str());
        ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
    }
    if (!ImGui::BeginDragDropTarget()) return;
    if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("GITREE_BRANCH")) {
        const char* source = static_cast<const char*>(payload->Data);
        if (source && source[0] != '\0') request_branch_merge(source, branch);
    }
    ImGui::EndDragDropTarget();
}

std::string branch_divergence_text(const std::string& branch, bool remote) {
    const auto& divergences = remote
        ? repo().remote_branch_divergence
        : repo().local_branch_divergence;
    const auto found = divergences.find(branch);
    if (found == divergences.end()) return {};

    std::string text;
    if (found->second.ahead > 0)
        text = std::to_string(found->second.ahead) + ICON_TB_ARROW_UP;
    if (found->second.behind > 0) {
        if (!text.empty()) text += "  ";
        text += std::to_string(found->second.behind) + ICON_TB_ARROW_DOWN;
    }
    return text;
}

void draw_branch_nodes(const BranchNode& parent, const char* branch_icon, const char* root_group_icon,
        bool remote, const std::string& id_path = {}, int depth = 0) {
    const ImU32 branch_icon_color = remote
        ? IM_COL32(82, 88, 98, 255)
        : IM_COL32(112, 118, 128, 255);
    for (const auto& [name, node] : parent.children) {
        const std::string id = id_path + "/" + name;
        if (!node.children.empty()) {
            const char* group_icon = depth == 0 ? root_group_icon : ICON_TB_FOLDER;
            const bool open = sidebar_collapse_row(
                id.c_str(), std::string(group_icon) + "  " + name, true, 0, false);
            if (open) {
                ImGui::Indent(ui(sidebar_nested_indent));
                if (node.branch) {
                    const std::string divergence = branch_divergence_text(node.full_name, remote);
                    sidebar_item_row(branch_icon, name, "branch" + id,
                        nullptr, 0, branch_icon_color, divergence,
                        !remote && node.full_name == repo().branch);
                    branch_drag_source(node.full_name, remote);
                    if (!remote) branch_drag_target(node.full_name);
                    branch_context(node.full_name, remote);
                }
                draw_branch_nodes(node, branch_icon, root_group_icon, remote, id, depth + 1);
                ImGui::Unindent(ui(sidebar_nested_indent));
            }
        } else {
            const std::string divergence = branch_divergence_text(node.full_name, remote);
            sidebar_item_row(branch_icon, name, id, nullptr, 0, branch_icon_color, divergence,
                !remote && node.full_name == repo().branch);
            branch_drag_source(node.full_name, remote);
            if (!remote) branch_drag_target(node.full_name);
            branch_context(node.full_name, remote);
        }
    }
}

void branch_section(const char* label, const std::vector<std::string>& branches, const char* branch_icon,
        const char* group_icon, const char* add_tooltip, const char* add_notice, bool remote,
        size_t section_index, float body_height, float next_height, float minimum_height, size_t next_section_index,
        bool persist_next_height, bool resizable) {
    const bool open = sidebar_section_header(label, static_cast<int>(branches.size()), add_tooltip, add_notice);
    if (open && body_height >= ui(1.0f)) {
        if (g_reset_repository_view) ImGui::SetNextWindowScroll({0.0f, 0.0f});
        ImGui::BeginChild((std::string(label) + "##body").c_str(), {-1, body_height});
        BranchNode root;
        for (const auto& branch : branches) {
            if (g_filter[0] && branch.find(g_filter.data()) == std::string::npos) continue;
            add_branch_node(root, branch);
        }
        ImGui::Indent(ui(sidebar_child_indent));
        draw_branch_nodes(root, branch_icon, group_icon, remote, label);
        ImGui::Unindent(ui(sidebar_child_indent));
        ImGui::EndChild();
        if (resizable)
            sidebar_section_splitter(label, section_index, body_height / g_ui_scale, next_height, minimum_height,
                next_section_index, persist_next_height);
    } else {
        ImGui::Separator();
    }
}

void draw_sidebar(float width) {
    ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(33 / 255.0f, 36 / 255.0f, 43 / 255.0f, 1.0f));
    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {ui(10.0f), ui(8.0f)});
    if (g_reset_repository_view) ImGui::SetNextWindowScroll({0.0f, 0.0f});
    ImGui::BeginChild("sidebar", {width, -ui(28.0f)}, ImGuiChildFlags_AlwaysUseWindowPadding,
        ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
    ImGui::PopStyleVar();
    const auto collapse_button = [](const char* icon, const char* tooltip) {
        const ImVec2 size{ui(22.0f), ui(22.0f)};
        ImGui::InvisibleButton("##sidebar_collapse", size);
        const bool clicked = ImGui::IsItemClicked();
        const ImVec2 minimum = ImGui::GetItemRectMin();
        const float icon_font_size = ImGui::GetFontSize() * 0.90f;
        const ImVec2 icon_size = g_regular_font->CalcTextSizeA(
            icon_font_size, std::numeric_limits<float>::max(), 0.0f, icon);
        ImGui::GetWindowDrawList()->AddText(g_regular_font, icon_font_size,
            {minimum.x + (size.x - icon_size.x) * 0.5f,
                minimum.y + (size.y - icon_size.y) * 0.5f},
            ImGui::IsItemHovered() ? IM_COL32(224, 229, 236, 255) : IM_COL32(158, 165, 176, 255), icon);
        if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) ImGui::SetTooltip("%s", tooltip);
        return clicked;
    };
    if (g_sidebar_collapsed) {
        ImGui::SetCursorPosX((ImGui::GetWindowWidth() - ui(22.0f)) * 0.5f);
        if (collapse_button(ICON_TB_CIRCLE_CHEVRON_RIGHT, "Expand sidebar")) {
            g_sidebar_collapsed = false;
            if (g_user_data) g_user_data->setSidebarCollapsed(false);
        }
        ImGui::Dummy({0.0f, ui(7.0f)});
        struct CompactSection {
            const char* icon;
            const char* label;
            size_t count;
        };
        const CompactSection sections[] = {
            {ICON_TB_DESKTOP, "Local branches", repo().local_branches.size()},
            {ICON_TB_CLOUD, "Remote branches", repo().remote_branches.size()},
            {ICON_TB_TREE, "Worktrees", repo().worktrees.size()},
            {ICON_TB_FOLDER_TREE, "Submodules", repo().submodules.size()},
        };
        for (size_t index = 0; index < std::size(sections); ++index) {
            ImGui::PushID(static_cast<int>(index));
            const ImVec2 row_size{-1.0f, ui(42.0f)};
            ImGui::InvisibleButton("##compact_section", row_size);
            const ImVec2 minimum = ImGui::GetItemRectMin();
            const ImVec2 maximum = ImGui::GetItemRectMax();
            const float icon_font_size = ui(19.0f);
            const ImVec2 icon_size = g_regular_font->CalcTextSizeA(icon_font_size,
                std::numeric_limits<float>::max(), 0.0f, sections[index].icon);
            if (ImGui::IsItemHovered())
                ImGui::GetWindowDrawList()->AddRectFilled(minimum, maximum, IM_COL32(51, 55, 63, 150), ui(4.0f));
            ImGui::GetWindowDrawList()->AddText(g_regular_font, icon_font_size,
                {minimum.x + (maximum.x - minimum.x - icon_size.x) * 0.5f, minimum.y + ui(1.0f)},
                IM_COL32(120, 126, 136, 255), sections[index].icon);
            const std::string count = std::to_string(sections[index].count);
            const ImVec2 count_size = ImGui::CalcTextSize(count.c_str());
            ImGui::GetWindowDrawList()->AddText(
                {minimum.x + (maximum.x - minimum.x - count_size.x) * 0.5f, minimum.y + ui(24.0f)},
                IM_COL32(82, 151, 245, 255), count.c_str());
            if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort))
                ImGui::SetTooltip("%s: %zu", sections[index].label, sections[index].count);
            if (ImGui::IsItemClicked()) {
                g_sidebar_collapsed = false;
                if (g_user_data) g_user_data->setSidebarCollapsed(false);
            }
            ImGui::PopID();
        }
        ImGui::EndChild();
        ImGui::PopStyleColor();
        return;
    }
    const int viewing_count = static_cast<int>(repo().local_branches.size() + repo().remote_branches.size() +
        repo().worktrees.size() + repo().submodules.size());
    if (collapse_button(ICON_TB_CIRCLE_CHEVRON_LEFT, "Collapse sidebar")) {
        g_sidebar_collapsed = true;
        if (g_user_data) g_user_data->setSidebarCollapsed(true);
    }
    ImGui::SameLine(0, ui(5.0f));
    ImGui::TextDisabled(ICON_TB_EYE "  VIEWING %d", viewing_count);
    ImGui::Dummy({0.0f, ui(3.0f)});
    ImGui::SetNextItemWidth(-1);
    ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
    ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, ui(4.0f));
    ImGui::PushStyleColor(ImGuiCol_FrameBg, IM_COL32(24, 27, 33, 255));
    ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, IM_COL32(27, 30, 36, 255));
    ImGui::PushStyleColor(ImGuiCol_FrameBgActive, IM_COL32(29, 32, 39, 255));
    ImGui::PushStyleColor(ImGuiCol_Border, IM_COL32(62, 67, 77, 255));
    ImGui::InputTextWithHint("##filter", "Filter",
        g_filter.data(), g_filter.size());
    const ImVec2 filter_min = ImGui::GetItemRectMin();
    const ImVec2 filter_max = ImGui::GetItemRectMax();
    ImDrawList* filter_draw = ImGui::GetWindowDrawList();
    const ImVec2 icon_size = ImGui::CalcTextSize(ICON_TB_MAGNIFYING_GLASS);
    const ImVec2 icon_position(
        filter_max.x - icon_size.x - ui(8.0f),
        filter_min.y + ((filter_max.y - filter_min.y) - icon_size.y) * 0.5f);
    filter_draw->AddText(icon_position, IM_COL32(136, 143, 154, 255), ICON_TB_MAGNIFYING_GLASS);
    ImGui::PopStyleColor(4);
    ImGui::PopStyleVar(2);
    ImGui::Dummy({0.0f, ui(5.0f)});
    ImGui::Separator();
    ImGui::Dummy({0.0f, ui(3.0f)});
    ImGui::BeginChild("sidebar_sections", {-1, -1}, ImGuiChildFlags_None,
        ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
    const ImVec2 section_spacing = ImGui::GetStyle().ItemSpacing;
    ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {section_spacing.x, 0.0f});

    constexpr const char* section_ids[] = {
        ICON_TB_DESKTOP "  LOCAL",
        ICON_TB_CLOUD "  REMOTE",
        ICON_TB_TREE "  WORKTREES",
        ICON_TB_FOLDER_TREE "  SUBMODULES",
    };
    std::array<bool, 4> section_open{};
    std::vector<size_t> open_indices;
    for (size_t index = 0; index < section_open.size(); ++index) {
        section_open[index] = sidebar_section_is_open(section_ids[index]);
        if (section_open[index]) open_indices.push_back(index);
    }
    const size_t last_open = open_indices.empty() ? section_open.size() : open_indices.back();
    const float header_space = (ui(24.0f) + 1.0f) * static_cast<float>(section_open.size());
    const float splitter_space = open_indices.size() > 1
        ? ui(5.0f) * static_cast<float>(open_indices.size() - 1)
        : 0.0f;
    const float body_space = std::max(0.0f, ImGui::GetContentRegionAvail().y - header_space - splitter_space);
    std::array<float, 4> section_heights{};
    std::array<size_t, 4> next_open_indices{};
    next_open_indices.fill(section_open.size());
    float minimum_height = 0.0f;
    if (!open_indices.empty()) {
        const float minimum_body = std::min(ui(42.0f), body_space / static_cast<float>(open_indices.size()));
        minimum_height = minimum_body / g_ui_scale;
        float remaining = body_space;
        for (size_t position = 0; position + 1 < open_indices.size(); ++position) {
            const size_t index = open_indices[position];
            const size_t remaining_sections = open_indices.size() - position - 1;
            const float maximum = std::max(minimum_body,
                remaining - minimum_body * static_cast<float>(remaining_sections));
            section_heights[index] = std::clamp(
                ui(g_user_data->sidebarSectionHeight(index)), minimum_body, maximum);
            remaining -= section_heights[index];
            next_open_indices[index] = open_indices[position + 1];
        }
        section_heights[last_open] = std::max(0.0f, remaining);
    }

    branch_section(ICON_TB_DESKTOP "  LOCAL", repo().local_branches, ICON_TB_CODE_BRANCH, ICON_TB_FOLDER,
        "Create local branch", "Create local branch", false, 0, section_heights[0],
        next_open_indices[0] < section_open.size() ? section_heights[next_open_indices[0]] / g_ui_scale : 0.0f,
        minimum_height, next_open_indices[0], next_open_indices[0] != last_open, section_open[0] && last_open != 0);
    branch_section(ICON_TB_CLOUD "  REMOTE", repo().remote_branches, ICON_TB_CODE_BRANCH, ICON_TB_GLOBE,
        "Add remote", "Add remote", true, 1, section_heights[1],
        next_open_indices[1] < section_open.size() ? section_heights[next_open_indices[1]] / g_ui_scale : 0.0f,
        minimum_height, next_open_indices[1], next_open_indices[1] != last_open, section_open[1] && last_open != 1);
    section(ICON_TB_TREE "  WORKTREES", repo().worktrees, ICON_TB_COMPUTER,
        "Add worktree", "Add worktree", SidebarItemKind::worktree, 2,
        section_heights[2],
        next_open_indices[2] < section_open.size() ? section_heights[next_open_indices[2]] / g_ui_scale : 0.0f,
        minimum_height, next_open_indices[2], next_open_indices[2] != last_open, section_open[2] && last_open != 2);
    section(ICON_TB_FOLDER_TREE "  SUBMODULES", repo().submodules, ICON_TB_FOLDER_TREE,
        "Add submodule", "Add submodule", SidebarItemKind::submodule, 3,
        section_heights[3], 0.0f, minimum_height, section_open.size(), false, false);
    ImGui::PopStyleVar();
    ImGui::EndChild();
    ImGui::EndChild();
    ImGui::PopStyleColor();
}

std::string ref_display_name(const RefBadge& badge) {
    std::string display_name = badge.name;
    if (badge.kind == RefKind::remote) {
        const size_t slash = display_name.find('/');
        if (slash != std::string::npos) display_name.erase(0, slash + 1);
    }
    return display_name;
}

float outline_icon_width(const char* icon) {
    return g_outline_icon_font
        ? g_outline_icon_font->CalcTextSizeA(g_outline_icon_size, 1000.0f, 0.0f, icon).x
        : 0.0f;
}

float ref_badge_width(const RefBadge& badge) {
    const std::string display_name = ref_display_name(badge);
    float width = ImGui::CalcTextSize(display_name.c_str()).x + ui(12.0f);
    if (badge.kind == RefKind::local) {
        width += outline_icon_width(ICON_TB_DEVICE_LAPTOP) + ui(6.0f);
        if (badge.current) width += outline_icon_width(ICON_TB_CHECK) + ui(5.0f);
    }
    if (badge.kind == RefKind::remote || badge.upstream)
        width += ui(5.0f) + outline_icon_width(ICON_TB_CLOUD);
    if (badge.kind == RefKind::tag) width += ui(5.0f) + outline_icon_width(ICON_TB_TAG);
    return width;
}

size_t primary_ref_index(const CommitInfo& commit) {
    const auto current = std::find_if(commit.refs.begin(), commit.refs.end(), [](const RefBadge& badge) {
        return badge.kind == RefKind::local && badge.current;
    });
    if (current != commit.refs.end())
        return static_cast<size_t>(std::distance(commit.refs.begin(), current));
    const auto local = std::find_if(commit.refs.begin(), commit.refs.end(), [](const RefBadge& badge) {
        return badge.kind == RefKind::local;
    });
    return local == commit.refs.end() ? 0 : static_cast<size_t>(std::distance(commit.refs.begin(), local));
}

float overflow_ref_badge_width(size_t count) {
    const std::string label = "+" + std::to_string(count);
    return ImGui::CalcTextSize(label.c_str()).x + ui(12.0f);
}

bool draw_overflow_ref_badge(size_t count, int widget_index, int lane, bool row_hovered,
        bool force_hover = false, ImDrawList* target_draw = nullptr,
        ImDrawFlags corners = ImDrawFlags_RoundCornersAll, int background_alpha = -1) {
    const std::string label = "+" + std::to_string(count);
    const ImVec2 chip_size{overflow_ref_badge_width(count), ui(20.0f)};
    ImGui::PushID(widget_index);
    ImGui::InvisibleButton("##more_refs", chip_size);
    const bool hovered = ImGui::IsItemHovered() || force_hover;
    const ImVec2 minimum = ImGui::GetItemRectMin();
    const ImVec2 maximum = ImGui::GetItemRectMax();
    ImDrawList* draw = target_draw ? target_draw : ImGui::GetWindowDrawList();
    const int alpha = background_alpha >= 0 ? background_alpha : hovered ? 155 : row_hovered ? 110 : 95;
    draw->AddRectFilled(minimum, maximum,
        GraphRenderer::laneColor(lane, alpha), ui(3.0f), corners);
    const ImVec2 text_size = ImGui::CalcTextSize(label.c_str());
    draw->AddText({minimum.x + (chip_size.x - text_size.x) * 0.5f,
        minimum.y + (chip_size.y - text_size.y) * 0.5f}, IM_COL32(197, 203, 212, 255), label.c_str());
    if (hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
    ImGui::PopID();
    return hovered;
}

void draw_ref_badge(const RefBadge& badge, int widget_index, int commit_index, int lane,
        bool row_hovered = false, bool force_hover = false, ImDrawList* target_draw = nullptr,
        ImDrawFlags corners = ImDrawFlags_RoundCornersAll, int background_alpha = -1,
        bool interactive = true, const ImVec2* explicit_position = nullptr) {
    const std::string display_name = ref_display_name(badge);

    const bool show_cloud = badge.kind == RefKind::remote || badge.upstream;
    const bool show_tag = badge.kind == RefKind::tag;
    const ImVec2 chip_size{ref_badge_width(badge), ui(20.0f)};
    ImGui::PushID(widget_index);
    if (interactive) ImGui::InvisibleButton("##ref_badge", chip_size);
    const bool item_hovered = (interactive && ImGui::IsItemHovered()) || force_hover;
    const bool switchable_branch = badge.kind == RefKind::local && !badge.current;
    if (interactive && ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
        repo().selected_commit = commit_index;
        clear_sidebar_filter();
    }
    if (interactive && switchable_branch && ImGui::IsItemHovered() &&
            ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
        request_branch_checkout(badge.name);
        clear_sidebar_filter();
    }
    const ImVec2 minimum = interactive ? ImGui::GetItemRectMin() :
        (explicit_position ? *explicit_position : ImGui::GetCursorScreenPos());
    const ImVec2 maximum = interactive ? ImGui::GetItemRectMax() :
        ImVec2{minimum.x + chip_size.x, minimum.y + chip_size.y};
    ImDrawList* draw = target_draw ? target_draw : ImGui::GetWindowDrawList();
    ImU32 background = GraphRenderer::laneColor(lane, background_alpha >= 0
        ? background_alpha : badge.current ? 205 : row_hovered ? 110 : 95);
    if (item_hovered && background_alpha < 0)
        background = GraphRenderer::laneColor(lane, badge.current ? 235 : 155);
    const float rounding = ui(3.0f);
    draw->AddRectFilled(minimum, maximum, background, rounding, corners);

    float x = minimum.x + ui(5.0f);
    const float text_y = minimum.y + (chip_size.y - ImGui::GetFontSize()) * 0.5f;
    const float icon_y = minimum.y + (chip_size.y - g_outline_icon_size) * 0.5f;
    const ImU32 color = badge.current ? IM_COL32(248, 250, 252, 255) : IM_COL32(197, 203, 212, 255);
    const auto draw_icon = [&](const char* icon) {
        if (!g_outline_icon_font) return;
        draw->AddText(g_outline_icon_font, g_outline_icon_size, {x, icon_y}, color, icon);
        x += outline_icon_width(icon) + ui(5.0f);
    };
    if (badge.current) draw_icon(ICON_TB_CHECK);
    draw->AddText({x, text_y}, color, display_name.c_str());
    x += ImGui::CalcTextSize(display_name.c_str()).x + ui(5.0f);
    if (badge.kind == RefKind::local) draw_icon(ICON_TB_DEVICE_LAPTOP);
    if (show_cloud) draw_icon(ICON_TB_CLOUD);
    if (show_tag) draw_icon(ICON_TB_TAG);

    if (interactive && ImGui::BeginPopupContextItem()) {
        if (switchable_branch && ImGui::MenuItem(ICON_TB_CODE_BRANCH "  Checkout")) {
            request_branch_checkout(badge.name);
            clear_sidebar_filter();
            ImGui::CloseCurrentPopup();
        }
        if (ImGui::MenuItem(ICON_TB_COPY "  Copy ref name")) copy_to_clipboard(badge.name, "ref name");
        ImGui::EndPopup();
    }
    if (interactive && ImGui::IsItemHovered()) {
        if (switchable_branch) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
        if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) {
            if (switchable_branch) ImGui::SetTooltip("Double-click to checkout %s", badge.name.c_str());
            else if (badge.current) ImGui::SetTooltip("Current branch: %s", badge.name.c_str());
            else ImGui::SetTooltip("%s", badge.name.c_str());
        }
    }
    ImGui::PopID();
}

void draw_commit_table() {
    const ScopedUiScale table_scale(1.0f);
    const bool find_shortcut = ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_F, false);
    if (find_shortcut) {
        if (g_commit_search_repository != &repo()) {
            g_commit_search.fill('\0');
            g_previous_commit_search.clear();
            g_commit_search_match = -1;
        }
        g_commit_search_repository = &repo();
        g_commit_search_open = true;
        g_focus_commit_search = true;
    }
    if (g_commit_search_open && g_commit_search_repository != &repo()) {
        g_commit_search_open = false;
        g_commit_search.fill('\0');
        g_previous_commit_search.clear();
        g_commit_search_match = -1;
    }

    if (!g_pending_commit_jump.empty()) {
        git_oid target{};
        const bool valid_target = git_oid_fromstr(&target, g_pending_commit_jump.c_str()) == 0;
        g_pending_commit_jump.clear();
        auto find_target = [&]() {
            return std::find_if(repo().commits.begin(), repo().commits.end(), [&](const CommitInfo& commit) {
                return valid_target && git_oid_equal(&commit.oid, &target) != 0;
            });
        };
        auto target_commit = find_target();
        while (valid_target && target_commit == repo().commits.end() && !repo().history_exhausted) {
            if (!g_git_manager->loadMoreCommits(repo(), 500, g_notice)) break;
            target_commit = find_target();
        }
        if (target_commit != repo().commits.end()) {
            repo().selected_commit = static_cast<int>(std::distance(repo().commits.begin(), target_commit));
            repo().scroll_to_commit = repo().selected_commit;
        } else if (valid_target) {
            g_notice = "Commit is not available in the graph";
        }
    }

    const auto commit_visible = [](const CommitInfo&) {
        return true;
    };

    const auto search_matches = [](const CommitInfo& commit, const char* query) {
        if (!query || !query[0]) return false;
        const auto contains = [query](const std::string& text) {
            return std::search(text.begin(), text.end(), query, query + std::strlen(query),
                [](unsigned char left, unsigned char right) {
                    return std::tolower(left) == std::tolower(right);
                }) != text.end();
        };
        return contains(commit.summary) || contains(commit.description);
    };

    std::vector<int> commit_search_matches;
    bool search_submitted = false;
    int search_direction = 0;
    if (g_commit_search_open) {
        ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.16f, 0.28f, 0.50f, 0.95f));
        ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.13f, 0.23f, 0.42f, 1.0f));
        ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
        ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1, 1, 1, 0.10f));
        ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, ui(3.0f));
        ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, ui(3.0f));
        ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {ui(5.0f), ui(4.0f)});
        ImGui::BeginChild("commit_search_bar", {-1, ui(34.0f)}, ImGuiChildFlags_None,
            ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
        ImGui::SetCursorPos({ui(9.0f), ui(5.0f)});
        ImGui::TextUnformatted(ICON_TB_MAGNIFYING_GLASS);
        ImGui::SameLine();
        const float search_controls_width = ui(170.0f);
        ImGui::SetNextItemWidth(std::max(ui(90.0f), ImGui::GetContentRegionAvail().x - search_controls_width));
        if (g_focus_commit_search) {
            ImGui::SetKeyboardFocusHere();
            g_focus_commit_search = false;
        }
        search_submitted = ImGui::InputTextWithHint("##commit_search", "find commit",
            g_commit_search.data(), g_commit_search.size(), ImGuiInputTextFlags_EnterReturnsTrue);
        const bool search_input_active = ImGui::IsItemActive();

        for (int index = 0; index < static_cast<int>(repo().commits.size()); ++index) {
            if (commit_visible(repo().commits[static_cast<size_t>(index)]) &&
                    search_matches(repo().commits[static_cast<size_t>(index)], g_commit_search.data()))
                commit_search_matches.push_back(index);
        }
        ImGui::SameLine();
        ImGui::Text("%d results", static_cast<int>(commit_search_matches.size()));
        ImGui::SameLine();
        if (ImGui::Button(ICON_TB_ARROW_UP "##previous_commit_match")) search_direction = -1;
        ImGui::SameLine();
        if (ImGui::Button(ICON_TB_ARROW_DOWN "##next_commit_match")) search_direction = 1;
        ImGui::SameLine();
        if (ImGui::Button(ICON_TB_XMARK "##close_commit_search") ||
                (search_input_active && ImGui::IsKeyPressed(ImGuiKey_Escape))) {
            g_commit_search_open = false;
            g_commit_search.fill('\0');
            g_previous_commit_search.clear();
            g_commit_search_match = -1;
        }
        ImGui::EndChild();
        ImGui::PopStyleVar(3);
        ImGui::PopStyleColor(4);
    }

    const std::string current_search = g_commit_search.data();
    const bool search_changed = current_search != g_previous_commit_search;
    if (search_changed) {
        g_previous_commit_search = current_search;
        g_commit_search_match = commit_search_matches.empty() ? -1 : 0;
        if (!commit_search_matches.empty()) {
            repo().selected_commit = commit_search_matches.front();
            repo().scroll_to_commit = commit_search_matches.front();
        }
    } else if (search_submitted && !commit_search_matches.empty()) {
        search_direction = 1;
    }
    if (search_direction != 0 && !commit_search_matches.empty()) {
        const int count = static_cast<int>(commit_search_matches.size());
        g_commit_search_match = (std::max(0, g_commit_search_match) + search_direction + count) % count;
    }
    if ((search_changed || search_direction != 0) && g_commit_search_match >= 0 &&
            g_commit_search_match < static_cast<int>(commit_search_matches.size())) {
        const int commit_index = commit_search_matches[static_cast<size_t>(g_commit_search_match)];
        repo().selected_commit = commit_index;
        repo().scroll_to_commit = commit_index;
    }

    float widest_reference_row = 0.0f;
    for (const CommitInfo& commit : repo().commits) {
        if (commit.refs.empty()) continue;
        const size_t primary = primary_ref_index(commit);
        float width = ref_badge_width(commit.refs[primary]);
        if (commit.refs.size() > 1)
            width += overflow_ref_badge_width(commit.refs.size() - 1);
        for (const RefBadge& badge : commit.refs) width = std::max(width, ref_badge_width(badge));
        widest_reference_row = std::max(widest_reference_row, width);
    }
    const float available_table_width = ImGui::GetContentRegionAvail().x;
    const float default_date_width = std::clamp(available_table_width * 0.20f, ui(130.0f), ui(175.0f));
    const float default_reference_width = std::clamp(
        widest_reference_row + ui(12.0f), ui(120.0f), available_table_width * 0.27f);
    const float desired_graph_width = GraphRenderer::requiredWidth(repo().commits, g_ui_scale) +
        (repo().working_files.empty() ? 0.0f : ui(22.0f));
    const float default_graph_width = std::min(desired_graph_width,
        std::max(ui(90.0f), available_table_width - default_reference_width - default_date_width - ui(170.0f)));
    float reference_width = default_reference_width;
    float graph_width = default_graph_width;
    float date_width = default_date_width;
    if (g_user_data) {
        reference_width = std::clamp(ui(g_user_data->commitTableColumnWidth(0)),
            ui(120.0f), std::max(ui(120.0f), available_table_width * 0.45f));
        graph_width = std::clamp(ui(g_user_data->commitTableColumnWidth(1)),
            ui(90.0f), std::max(ui(90.0f), available_table_width * 0.35f));
        date_width = std::clamp(ui(g_user_data->commitTableColumnWidth(3)),
            ui(130.0f), std::max(ui(130.0f), available_table_width * 0.30f));
        if (g_user_data->commitTableColumnWidth(0) <= 0.0f) reference_width = default_reference_width;
        if (g_user_data->commitTableColumnWidth(1) <= 0.0f) graph_width = default_graph_width;
        if (g_user_data->commitTableColumnWidth(3) <= 0.0f) date_width = default_date_width;
    }
    if (reference_width + graph_width + date_width > available_table_width - ui(120.0f)) {
        const float scale = std::max(0.5f, (available_table_width - ui(120.0f)) /
            std::max(ui(1.0f), reference_width + graph_width + date_width));
        reference_width *= scale;
        graph_width *= scale;
        date_width *= scale;
    }
    const float message_width = std::max(ui(120.0f),
        available_table_width - reference_width - graph_width - date_width);
    const float chip_line_width = std::max(ui(40.0f), reference_width - ui(12.0f));
    std::vector<float> row_heights(repo().commits.size(), 0.0f);
    std::unordered_map<std::string, int> commit_rows;
    commit_rows.reserve(repo().commits.size());
    for (int index = 0; index < static_cast<int>(repo().commits.size()); ++index) {
        char oid[GIT_OID_SHA1_HEXSIZE + 1]{};
        git_oid_tostr(oid, sizeof(oid), &repo().commits[static_cast<size_t>(index)].oid);
        commit_rows.emplace(oid, index);
    }
    std::vector<std::vector<int>> parent_rows(repo().commits.size());
    for (size_t index = 0; index < repo().commits.size(); ++index) {
        for (const git_oid& parent_oid : repo().commits[index].parent_ids) {
            char oid[GIT_OID_SHA1_HEXSIZE + 1]{};
            git_oid_tostr(oid, sizeof(oid), &parent_oid);
            const auto parent = commit_rows.find(oid);
            if (parent != commit_rows.end()) parent_rows[index].push_back(parent->second);
        }
    }
    for (size_t index = 0; index < repo().commits.size(); ++index) {
        const CommitInfo& commit = repo().commits[index];
        if (!commit_visible(commit)) continue;
        const int lines = commit.refs.empty() ? 0 : 1;
        row_heights[index] = std::max(ui(24.0f), lines * ui(23.0f) + ui(1.0f));
        if (g_inline_branch_repository == &repo() && g_inline_branch_commit == static_cast<int>(index))
            row_heights[index] = std::max(row_heights[index], ui(30.0f));
    }

    ImGuiTableFlags flags = ImGuiTableFlags_RowBg |
        ImGuiTableFlags_ScrollY | ImGuiTableFlags_Resizable |
        ImGuiTableFlags_NoBordersInBody | ImGuiTableFlags_NoBordersInBodyUntilResize |
        ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoSavedSettings;
    ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, {ImGui::GetStyle().CellPadding.x, ui(1.0f)});
    if (!ImGui::BeginTable("commits", 4, flags, {-1, -1})) {
        ImGui::PopStyleVar();
        return;
    }
    ImGui::TableSetupScrollFreeze(0, 1);
    const GraphRenderer graph(g_ui_scale);
    ImGui::TableSetupColumn("BRANCH / TAG", ImGuiTableColumnFlags_WidthFixed, reference_width);
    ImGui::TableSetupColumn("GRAPH", ImGuiTableColumnFlags_WidthFixed, graph_width);
    ImGui::TableSetupColumn("COMMIT MESSAGE", ImGuiTableColumnFlags_WidthFixed, message_width);
    ImGui::TableSetupColumn("COMMIT DATE / TIME", ImGuiTableColumnFlags_WidthFixed, date_width);
    ImGui::PushFont(g_regular_font, ImGui::GetStyle().FontSizeBase * 0.5f);
    ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(134, 140, 151, 255));
    ImGui::TableHeadersRow();
    if (g_user_data) {
        for (int column = 0; column < 4; ++column)
            g_user_data->setCommitTableColumnWidth(static_cast<size_t>(column),
                ImGui::GetColumnWidth(column) / g_ui_scale);
    }
    ImGui::PopStyleColor();
    ImGui::PopFont();
    const float table_body_clip_top = ImGui::GetItemRectMax().y;
    const float table_body_clip_bottom = ImGui::GetWindowPos().y + ImGui::GetWindowHeight();
    const auto graph_row_interaction = [&](const char* id, bool selected, float row_height,
            bool& hovered) {
        const ImVec2 cursor = ImGui::GetCursorScreenPos();
        const float row_left = cursor.x - ImGui::GetStyle().CellPadding.x;
        const float row_top = cursor.y - ImGui::GetStyle().CellPadding.y;
        const float message_left = row_left + reference_width + graph_width;
        const float row_right = message_left + message_width + date_width +
            ImGui::GetStyle().CellPadding.x * 2.0f;
        const ImVec2 mouse = ImGui::GetIO().MousePos;
        hovered = ImGui::IsWindowHovered() && mouse.x >= row_left && mouse.x < row_right &&
            mouse.y >= row_top && mouse.y < row_top + row_height;
        if (selected || hovered) {
            ImDrawList* draw = ImGui::GetWindowDrawList();
            draw->PushClipRect({message_left, table_body_clip_top},
                {row_right, table_body_clip_bottom}, false);
            draw->AddRectFilled({message_left, row_top}, {row_right, row_top + row_height},
                selected ? IM_COL32(45, 58, 84, 155) : IM_COL32(70, 77, 90, 55));
            draw->PopClipRect();
        }
        ImGui::InvisibleButton(id, {-1, row_height});
        return ImGui::IsItemClicked(ImGuiMouseButton_Left) ||
            (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left));
    };
    if (!repo().working_files.empty()) {
        ImGui::TableNextRow(0, ui(24.0f));
        ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, IM_COL32(34, 37, 44, 42));
        ImGui::TableSetColumnIndex(0);
        bool pending_hovered = false;
        if (graph_row_interaction("##working_tree", repo().selected_commit == -1,
                ui(24.0f), pending_hovered))
            repo().selected_commit = -1;
        ImGui::TableSetColumnIndex(1);
        const ImVec2 position = ImGui::GetCursorScreenPos();
        ImDrawList* draw = ImGui::GetWindowDrawList();
        const int first_lane = repo().commits.empty() ? 0 : repo().commits.front().lane;
        const float lane_x = position.x + ui(17.0f) + ui(22.0f) * first_lane;
        const ImVec2 center{lane_x + ui(22.0f), position.y + ui(10.0f)};
        if (!repo().commits.empty()) {
            const float next_center_y = position.y + ui(24.0f) +
                std::max(ui(1.0f), row_heights.front() - ImGui::GetStyle().CellPadding.y * 2.0f) * 0.5f;
            const ImVec2 end{lane_x, next_center_y};
            const float turn_y = center.y + (end.y - center.y) * 0.55f;
            draw->PushClipRect({position.x, table_body_clip_top},
                {position.x + graph_width, table_body_clip_bottom}, false);
            draw_dotted_bezier(draw, center, {center.x, turn_y}, {end.x, turn_y}, end,
                IM_COL32(23, 181, 204, 235), ui(1.15f), ui(4.2f));
            draw->PopClipRect();
        }
        draw->AddCircle(center, ui(6), IM_COL32(23, 181, 204, 220), 12, ui(1.5f));
        ImGui::Dummy({0, ui(20)});
        ImGui::TableSetColumnIndex(2);
        int modified_count = 0;
        int added_count = 0;
        int deleted_count = 0;
        for (const WorkingFile& file : repo().working_files) {
            if (file.kind == FileChangeKind::added) ++added_count;
            else if (file.kind == FileChangeKind::deleted) ++deleted_count;
            else ++modified_count;
        }
        const std::string modified_text = std::string(ICON_TB_PEN " ") + std::to_string(modified_count);
        const std::string added_text = std::string(ICON_TB_PLUS " ") + std::to_string(added_count);
        const std::string deleted_text = std::string(ICON_TB_MINUS " ") + std::to_string(deleted_count);
        float count_width = 0.0f;
        int visible_counts = 0;
        const auto measure_count = [&](int count, const std::string& text) {
            if (!count) return;
            if (visible_counts++) count_width += ui(10.0f);
            count_width += ImGui::CalcTextSize(text.c_str()).x;
        };
        measure_count(modified_count, modified_text);
        measure_count(added_count, added_text);
        measure_count(deleted_count, deleted_text);
        const float pending_width = std::clamp(
            ImGui::GetContentRegionAvail().x - count_width - ui(12.0f), ui(78.0f), ui(105.0f));
        ImGui::SetNextItemWidth(pending_width);
        ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.075f, 0.085f, 0.10f, 0.82f));
        ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(0.10f, 0.115f, 0.14f, 0.92f));
        ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(0.11f, 0.125f, 0.15f, 1.0f));
        ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, ui(3.0f));
        ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {ui(7.0f), ui(1.0f)});
        ImGui::InputTextWithHint("##pending_commit_summary", "// WIP",
            g_commit_summary.data(), g_commit_summary.size());
        ImGui::PopStyleVar(2);
        ImGui::PopStyleColor(3);
        bool first_count = true;
        const auto draw_count = [&](int count, FileChangeKind kind, const std::string& text) {
            if (!count) return;
            ImGui::SameLine(0, first_count ? ui(8.0f) : ui(10.0f));
            const ScopedUiScale counter_scale(0.86f);
            ImVec4 color = change_color(kind);
            color.x *= 0.84f;
            color.y *= 0.84f;
            color.z *= 0.84f;
            ImGui::TextColored(color, "%s", text.c_str());
            first_count = false;
        };
        draw_count(modified_count, FileChangeKind::modified, modified_text);
        draw_count(added_count, FileChangeKind::added, added_text);
        draw_count(deleted_count, FileChangeKind::deleted, deleted_text);
    }
    bool submit_inline_branch = false;
    for (int i = 0; i < static_cast<int>(repo().commits.size()); ++i) {
        const auto& commit = repo().commits[i];
        if (!commit_visible(commit)) continue;
        const float row_height = row_heights[static_cast<size_t>(i)];
        ImGui::TableNextRow(0, row_height);
        ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,
            (i & 1) == 0 ? IM_COL32(35, 38, 45, 34) : IM_COL32(32, 35, 41, 18));
        ImGui::TableSetColumnIndex(0);
        if (repo().scroll_to_commit == i) {
            ImGui::SetScrollFromPosY(ImGui::GetCursorScreenPos().y, 0.5f);
            repo().scroll_to_commit = -1;
        }
        if (!ImGui::IsRectVisible({ui(1.0f), row_height})) continue;
        const ImVec2 reference_origin = ImGui::GetCursorScreenPos();
        std::string row_id = "##commit" + std::to_string(i);
        bool row_hovered = false;
        if (graph_row_interaction(row_id.c_str(), repo().selected_commit == i, row_height, row_hovered)) {
            repo().selected_commit = i;
            clear_sidebar_filter();
        }
        const std::string commit_hash = oid_string(commit.oid);
        if (ImGui::BeginPopupContextItem()) {
            repo().selected_commit = i;
            if (ImGui::MenuItem(ICON_TB_ARROW_DOWN "  Checkout this commit")) {
                run_repository_action({"checkout", commit_hash}, "Checked out commit " + commit.short_id, commit_hash);
                ImGui::CloseCurrentPopup();
            }
            if (ImGui::MenuItem(ICON_TB_TREE "  Create worktree from this commit")) {
                g_git_path.fill('\0');
                g_git_value.fill('\0');
                std::snprintf(g_git_value.data(), g_git_value.size(), "%s", commit_hash.c_str());
                g_worktree_add_popup = true;
                ImGui::CloseCurrentPopup();
            }
            ImGui::Separator();
            if (ImGui::MenuItem(ICON_TB_COPY "  Copy commit hash")) {
                copy_to_clipboard(commit_hash, "commit hash");
            }
            if (ImGui::MenuItem(ICON_TB_COPY "  Copy commit message"))
                copy_to_clipboard(commit.summary, "commit message");
            ImGui::Separator();
            if (ImGui::MenuItem(ICON_TB_TAG "  Create tag here")) {
                g_git_target = commit_hash;
                g_tag_create_popup = true;
            }
            if (ImGui::MenuItem(ICON_TB_CODE_BRANCH "  Create branch here")) {
                g_git_target = commit_hash;
                g_git_target_label = std::string("commit ") + commit.short_id;
                g_branch_create_popup = true;
            }
            ImGui::Separator();
            if (ImGui::MenuItem(ICON_TB_ARROW_RIGHT_ARROW_LEFT "  Cherry-pick commit")) {
                run_repository_action({"cherry-pick", commit_hash}, "Cherry-picked " + commit.short_id);
                ImGui::CloseCurrentPopup();
            }
            if (ImGui::BeginMenu(ICON_TB_ROTATE_LEFT "  Reset current branch to this commit")) {
                if (ImGui::MenuItem("Soft reset")) {
                    run_repository_action({"reset", "--soft", commit_hash},
                        "Soft reset to " + commit.short_id, commit_hash);
                    ImGui::CloseCurrentPopup();
                }
                if (ImGui::MenuItem("Mixed reset")) {
                    run_repository_action({"reset", "--mixed", commit_hash},
                        "Reset to " + commit.short_id, commit_hash);
                    ImGui::CloseCurrentPopup();
                }
                if (ImGui::MenuItem("Hard reset")) {
                    run_repository_action({"reset", "--hard", commit_hash},
                        "Hard reset to " + commit.short_id, commit_hash);
                    ImGui::CloseCurrentPopup();
                }
                ImGui::EndMenu();
            }
            if (ImGui::MenuItem(ICON_TB_ROTATE_RIGHT "  Revert commit")) {
                run_repository_action({"revert", "--no-edit", commit_hash}, "Reverted " + commit.short_id);
                ImGui::CloseCurrentPopup();
            }
            if (ImGui::MenuItem(ICON_TB_FILE "  Copy patch for commit")) {
                std::string patch;
                if (g_git_manager->captureGit(repo(), {"format-patch", "-1", commit_hash, "--stdout"}, patch, g_notice))
                    copy_to_clipboard(patch, "commit patch");
            }
            ImGui::EndPopup();
        }
        float chip_x = reference_origin.x + ui(3.0f);
        float chip_y = reference_origin.y + ui(0.5f);
        const bool editing_branch = g_inline_branch_repository == &repo() && g_inline_branch_commit == i;
        if (editing_branch) {
            ImGui::SetCursorScreenPos({reference_origin.x + ui(3.0f), reference_origin.y + ui(1.0f)});
            ImGui::SetNextItemWidth(std::clamp(chip_line_width - ui(4.0f), ui(90.0f), ui(170.0f)));
            ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.08f, 0.10f, 0.14f, 1.0f));
            ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.16f, 0.48f, 0.95f, 1.0f));
            ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, ui(1.0f));
            ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {ui(7.0f), ui(3.0f)});
            if (g_focus_inline_branch) {
                ImGui::SetKeyboardFocusHere();
                g_focus_inline_branch = false;
            }
            submit_inline_branch = ImGui::InputTextWithHint("##inline_branch_name", "enter branch name",
                g_inline_branch_name.data(), g_inline_branch_name.size(), ImGuiInputTextFlags_EnterReturnsTrue);
            ImGui::PopStyleVar(2);
            ImGui::PopStyleColor(2);
            if (ImGui::IsKeyPressed(ImGuiKey_Escape)) cancel_inline_branch();
        }
        if (!editing_branch && !commit.refs.empty()) {
            const size_t primary = primary_ref_index(commit);
            const bool refs_expanded = g_expanded_refs_repository == &repo() && g_expanded_refs_commit == i;
            const auto draw_badge = [&](size_t ref_index, ImDrawList* target = nullptr,
                    ImDrawFlags corners = ImDrawFlags_RoundCornersAll, bool force_hover = false,
                    int background_alpha = -1, bool interactive = true) {
                const ImVec2 badge_position{chip_x, chip_y};
                if (interactive) ImGui::SetCursorScreenPos(badge_position);
                draw_ref_badge(commit.refs[ref_index], i * 1000 + static_cast<int>(ref_index),
                    i, commit.graph_color, row_hovered, force_hover, target, corners,
                    background_alpha, interactive, interactive ? nullptr : &badge_position);
            };
            if (refs_expanded) {
                float stack_width = ref_badge_width(commit.refs[primary]) +
                    overflow_ref_badge_width(commit.refs.size() - 1);
                for (const RefBadge& badge : commit.refs)
                    stack_width = std::max(stack_width, ref_badge_width(badge));
                const float stack_height = ui(20.0f) + ui(23.0f) * (commit.refs.size() - 1);
                const ImVec2 mouse = ImGui::GetIO().MousePos;
                const bool stack_hovered = mouse.x >= reference_origin.x + ui(3.0f) &&
                    mouse.x < reference_origin.x + ui(3.0f) + stack_width &&
                    mouse.y >= reference_origin.y + ui(0.5f) &&
                    mouse.y < reference_origin.y + ui(0.5f) + stack_height;
                ImDrawList* overlay = ImGui::GetForegroundDrawList();
                overlay->PushClipRect({reference_origin.x, table_body_clip_top},
                    {reference_origin.x + reference_width, table_body_clip_bottom}, false);
                draw_badge(primary, overlay, ImDrawFlags_RoundCornersAll, stack_hovered, -1, false);
                for (size_t ref_index = 0; ref_index < commit.refs.size(); ++ref_index) {
                    if (ref_index == primary) continue;
                    chip_y += ui(23.0f);
                    chip_x = reference_origin.x + ui(3.0f);
                    draw_badge(ref_index, overlay, ImDrawFlags_RoundCornersAll,
                        stack_hovered, -1, false);
                }
                overlay->PopClipRect();
                if (!stack_hovered) {
                    g_expanded_refs_repository = nullptr;
                    g_expanded_refs_commit = -1;
                }
            } else if (commit.refs.size() == 1) {
                draw_badge(primary);
            } else {
                const float primary_width = ref_badge_width(commit.refs[primary]);
                const float more_width = overflow_ref_badge_width(commit.refs.size() - 1);
                const float chip_gap = ui(4.0f);
                const ImVec2 mouse = ImGui::GetIO().MousePos;
                const bool group_hovered = mouse.x >= chip_x && mouse.x < chip_x + primary_width + chip_gap + more_width &&
                    mouse.y >= chip_y && mouse.y < chip_y + ui(20.0f);
                const int group_alpha = group_hovered ?
                    (commit.refs[primary].current ? 235 : 155) :
                    (commit.refs[primary].current ? 205 : row_hovered ? 110 : 95);
                draw_badge(primary, nullptr, ImDrawFlags_RoundCornersAll, group_hovered, group_alpha);
                chip_x += primary_width + chip_gap;
                ImGui::SetCursorScreenPos({chip_x, chip_y});
                if (draw_overflow_ref_badge(commit.refs.size() - 1, i * 1000 + 999,
                        commit.graph_color, row_hovered, group_hovered, nullptr,
                        ImDrawFlags_RoundCornersAll, group_alpha) || group_hovered) {
                    g_expanded_refs_repository = &repo();
                    g_expanded_refs_commit = i;
                }
            }
        }
        ImGui::TableSetColumnIndex(1);
        graph.drawRow(i, commit, repo().commits, row_heights, parent_rows, g_avatar_cache,
            nullptr);
        ImGui::TableSetColumnIndex(2);
        ImGui::PushID(i);
        const ImVec2 message_position = ImGui::GetCursorScreenPos();
        ImGui::InvisibleButton("##commit_message", {-1, std::max(ImGui::GetTextLineHeight(), row_height - ui(2.0f))});
        if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
            repo().selected_commit = i;
            clear_sidebar_filter();
        }
        ImDrawList* message_draw = ImGui::GetWindowDrawList();
        message_draw->PushClipRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), true);
        const bool searching_commits = g_commit_search_open && g_commit_search[0] != '\0';
        const bool focused_search_match = searching_commits && g_commit_search_match >= 0 &&
            g_commit_search_match < static_cast<int>(commit_search_matches.size()) &&
            commit_search_matches[static_cast<size_t>(g_commit_search_match)] == i;
        const bool other_search_match = searching_commits && search_matches(commit, g_commit_search.data());
        const ImU32 summary_color = !searching_commits || focused_search_match
            ? ImGui::GetColorU32(ImGuiCol_Text)
            : other_search_match ? IM_COL32(170, 175, 184, 180) : IM_COL32(112, 117, 126, 90);
        const ImU32 description_color = !searching_commits || focused_search_match
            ? IM_COL32(143, 149, 160, 255)
            : other_search_match ? IM_COL32(130, 136, 147, 155) : IM_COL32(100, 105, 114, 70);
        message_draw->AddText(message_position, summary_color, commit.summary.c_str());
        if (!commit.description.empty()) {
            const float summary_width = ImGui::CalcTextSize(commit.summary.c_str()).x;
            message_draw->AddText({message_position.x + summary_width + ui(12.0f), message_position.y},
                description_color, commit.description.c_str());
        }
        message_draw->PopClipRect();
        ImGui::PopID();
        ImGui::TableSetColumnIndex(3);
        ImGui::PushFont(nullptr, ImGui::GetStyle().FontSizeBase * 0.75f);
        ImGui::TextDisabled("%s", commit.date.c_str());
        ImGui::PopFont();
    }
    const bool load_more_history = !repo().history_exhausted && repo().commit_walk &&
        ImGui::GetScrollMaxY() - ImGui::GetScrollY() < ImGui::GetWindowHeight() * 1.5f;
    ImGui::EndTable();
    ImGui::PopStyleVar();
    if (load_more_history) g_git_manager->loadMoreCommits(repo(), 500, g_notice);
    if (submit_inline_branch && g_inline_branch_name[0] != '\0') {
        const std::string name = g_inline_branch_name.data();
        const std::string target = g_inline_branch_target;
        if (g_git_manager->createBranch(repo(), name, target, true, g_notice)) cancel_inline_branch();
        else g_focus_inline_branch = true;
    }
}

const char* change_icon(FileChangeKind kind) {
    if (kind == FileChangeKind::added) return ICON_TB_PLUS;
    if (kind == FileChangeKind::deleted) return ICON_TB_MINUS;
    if (kind == FileChangeKind::renamed) return ICON_TB_ARROW_RIGHT_ARROW_LEFT;
    if (kind == FileChangeKind::modified) return ICON_TB_PEN;
    return ICON_TB_FILE;
}

ImVec4 change_color(FileChangeKind kind) {
    if (kind == FileChangeKind::added) return {0.31f, 0.82f, 0.43f, 1};
    if (kind == FileChangeKind::deleted) return {0.91f, 0.35f, 0.35f, 1};
    if (kind == FileChangeKind::renamed) return {0.38f, 0.67f, 0.92f, 1};
    if (kind == FileChangeKind::modified) return {0.94f, 0.66f, 0.25f, 1};
    return {0.58f, 0.61f, 0.67f, 1};
}

struct FileTypeBadge {
    const char* icon = nullptr;
    ImU32 color = IM_COL32(178, 184, 194, 255);
};

FileTypeBadge file_type_badge(std::string_view path) {
    const std::filesystem::path file_path(path);
    const auto extension = file_path.extension().string();
    std::string lowered = extension;
    std::transform(lowered.begin(), lowered.end(), lowered.begin(), [](unsigned char value) {
        return static_cast<char>(std::tolower(value));
    });
    std::string filename = file_path.filename().string();
    std::transform(filename.begin(), filename.end(), filename.begin(), [](unsigned char value) {
        return static_cast<char>(std::tolower(value));
    });
    if (lowered == ".c") return {ICON_TB_FILE_CODE, IM_COL32(94, 140, 214, 255)};
    if (lowered == ".h") return {ICON_TB_FILE_CODE, IM_COL32(112, 138, 192, 255)};
    if (lowered == ".cc" || lowered == ".cpp" || lowered == ".cxx" ||
            lowered == ".hh" || lowered == ".hpp" || lowered == ".hxx")
        return {ICON_TB_BRAND_CPP, IM_COL32(74, 132, 225, 255)};
    if (lowered == ".cs") return {ICON_TB_BRAND_C_SHARP, IM_COL32(143, 101, 217, 255)};
    if (lowered == ".rs") return {ICON_TB_BRAND_RUST, IM_COL32(196, 122, 72, 255)};
    if (lowered == ".go") return {ICON_TB_BRAND_GOLANG, IM_COL32(71, 188, 202, 255)};
    if (lowered == ".py" || lowered == ".pyw") return {ICON_TB_BRAND_PYTHON, IM_COL32(94, 154, 222, 255)};
    if (lowered == ".jsx") return {ICON_TB_BRAND_JAVASCRIPT, IM_COL32(233, 212, 96, 255)};
    if (lowered == ".tsx") return {ICON_TB_BRAND_TYPESCRIPT, IM_COL32(80, 149, 232, 255)};
    if (lowered == ".js" || lowered == ".mjs" || lowered == ".cjs")
        return {ICON_TB_BRAND_JAVASCRIPT, IM_COL32(233, 212, 96, 255)};
    if (lowered == ".ts") return {ICON_TB_BRAND_TYPESCRIPT, IM_COL32(80, 149, 232, 255)};
    if (lowered == ".lua") return {ICON_TB_FILE_CODE, IM_COL32(104, 126, 212, 255)};
    if (lowered == ".sh" || lowered == ".bash" || lowered == ".zsh") return {ICON_TB_TERMINAL_2, IM_COL32(103, 166, 117, 255)};
    if (lowered == ".bat" || lowered == ".cmd") return {ICON_TB_TERMINAL_2, IM_COL32(142, 142, 154, 255)};
    if (lowered == ".ps1") return {ICON_TB_BRAND_POWERSHELL, IM_COL32(84, 154, 220, 255)};
    if (lowered == ".json") return {ICON_TB_JSON, IM_COL32(208, 184, 95, 255)};
    if (lowered == ".xml" || lowered == ".html" || lowered == ".htm") return {ICON_TB_BRAND_HTML5, IM_COL32(221, 117, 88, 255)};
    if (lowered == ".css" || lowered == ".scss") return {ICON_TB_BRAND_CSS3, IM_COL32(99, 133, 221, 255)};
    if (lowered == ".md") return {ICON_TB_MARKDOWN, IM_COL32(134, 144, 160, 255)};
    if (lowered == ".yml" || lowered == ".yaml" || lowered == ".toml") return {ICON_TB_FILE_TEXT, IM_COL32(170, 121, 108, 255)};
    if (lowered == ".sql") return {ICON_TB_FILE_TYPE_SQL, IM_COL32(104, 178, 184, 255)};
    if (filename == "package.json" || filename == "package-lock.json" || filename == ".npmrc")
        return {ICON_TB_BRAND_NODEJS, IM_COL32(101, 184, 108, 255)};
    return {};
}

float file_type_badge_width(const FileTypeBadge& badge) {
    return badge.icon ? outline_icon_width(badge.icon) + ui(6.0f) : 0.0f;
}

void draw_file_type_badge(ImDrawList* draw, const ImVec2& minimum, float y, const FileTypeBadge& badge) {
    if (!badge.icon) return;
    const float icon_x = minimum.x + ui(21.0f);
    draw->AddText(g_outline_icon_font ? g_outline_icon_font : ImGui::GetFont(),
        g_outline_icon_size, {icon_x, y}, badge.color, badge.icon);
}

float file_action_button_width(const char* label) {
    return ImGui::CalcTextSize(label).x + ui(16.0f);
}

bool file_action_button(const char* label, bool positive) {
    if (positive) {
        ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.13f, 0.23f, 0.17f, 1.0f));
        ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.16f, 0.29f, 0.21f, 1.0f));
        ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.18f, 0.33f, 0.24f, 1.0f));
        ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.20f, 0.34f, 0.25f, 1.0f));
    } else {
        ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.24f, 0.11f, 0.12f, 1.0f));
        ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.30f, 0.14f, 0.15f, 1.0f));
        ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.35f, 0.16f, 0.17f, 1.0f));
        ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.34f, 0.18f, 0.19f, 1.0f));
    }
    ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, ui(1.0f));
    ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {ui(7.0f), ui(2.0f)});
    ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, ui(3.0f));
    const bool clicked = ImGui::Button(label, {file_action_button_width(label), ui(21.0f)});
    ImGui::PopStyleVar(3);
    ImGui::PopStyleColor(4);
    return clicked;
}

float file_section_toggle_width(const std::string& label) {
    return ImGui::CalcTextSize(label.c_str()).x + ui(12.0f);
}

bool file_section_toggle(const char* id, const std::string& label) {
    const float width = file_section_toggle_width(label);
    return ImGui::InvisibleButton(id, {width, ui(24.0f)});
}

void draw_file_row(const std::string& path, FileChangeKind kind, int id,
        bool working_file = false, bool staged = false, const std::string& action_path = {},
        const std::string& commit_id = {}) {
    ImGui::PushID(id);
    ImGui::SetNextItemAllowOverlap();
    ImGui::InvisibleButton("##file", {-1, ui(25.0f)});
    const std::string& git_path = action_path.empty() ? path : action_path;
    const bool row_clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
    const bool row_hovered = ImGui::IsItemHovered();
    const ImVec2 minimum = ImGui::GetItemRectMin();
    const ImVec2 maximum = ImGui::GetItemRectMax();
    ImDrawList* draw = ImGui::GetWindowDrawList();
    if (row_hovered) draw->AddRectFilled(minimum, maximum, IM_COL32(48, 52, 60, 255));
    const float y = minimum.y + (maximum.y - minimum.y - ImGui::GetFontSize()) * 0.5f;
    const bool show_file_type_icon = !working_file && !commit_id.empty() && g_file_view_mode == FileViewMode::tree;
    const FileTypeBadge badge = show_file_type_icon ? file_type_badge(git_path) : FileTypeBadge{};
    const float reserved_action_width = working_file
        ? std::max(file_action_button_width("Stage File"), file_action_button_width("Unstage File")) + ui(8.0f)
        : 0.0f;
    const float text_right = maximum.x - reserved_action_width;
    draw->AddText({minimum.x + ui(4.0f), y}, ImGui::ColorConvertFloat4ToU32(change_color(kind)), change_icon(kind));
    draw_file_type_badge(draw, minimum, y, badge);
    const float text_x = minimum.x + ui(20.0f) + (badge.icon ? file_type_badge_width(badge) + ui(4.0f) : 0.0f);
    draw->PushClipRect({text_x, minimum.y}, {std::max(text_x, text_right), maximum.y}, true);
    draw->AddText({text_x, y}, IM_COL32(205, 209, 216, 255), path.c_str());
    draw->PopClipRect();
    if (ImGui::BeginPopupContextItem()) {
        if (working_file && !staged && ImGui::MenuItem(ICON_TB_PLUS "  Stage file"))
            g_git_manager->stageFile(repo(), git_path, g_notice);
        if (working_file && staged && ImGui::MenuItem(ICON_TB_MINUS "  Unstage file"))
            g_git_manager->unstageFile(repo(), git_path, g_notice);
        if (working_file && !staged && ImGui::MenuItem(ICON_TB_TRASH_CAN "  Discard changes"))
            g_git_manager->discardFile(repo(), git_path, g_notice);
        if (working_file) ImGui::Separator();
        if (ImGui::MenuItem(ICON_TB_COPY "  Copy path")) copy_to_clipboard(path, "path");
        ImGui::EndPopup();
    }
    bool row_action_clicked = false;
    if (working_file && row_hovered) {
        const char* label = staged ? "Unstage File" : "Stage File";
        const float button_width = file_action_button_width(label);
        ImGui::SetCursorScreenPos({maximum.x - button_width - ui(4.0f), minimum.y + ui(2.5f)});
        row_action_clicked = file_action_button(label, !staged);
    }
    if (row_action_clicked) {
        if (staged) g_git_manager->unstageFile(repo(), git_path, g_notice);
        else g_git_manager->stageFile(repo(), git_path, g_notice);
    } else if (row_clicked) {
        if (commit_id.empty()) g_diff_viewer.open(repo(), *g_git_manager, git_path, staged, g_notice);
        else g_diff_viewer.openCommit(repo(), *g_git_manager, git_path, commit_id, g_notice);
    }
    ImGui::PopID();
}

void draw_file_toolbar(bool show_view_all) {
    if (show_view_all && g_view_all_files) g_file_view_mode = FileViewMode::tree;
    if (ImGui::SmallButton(g_file_sort_ascending ? ICON_TB_ARROW_DOWN_A_Z : ICON_TB_ARROW_DOWN_Z_A))
        g_file_sort_ascending = !g_file_sort_ascending;
    if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort))
        ImGui::SetTooltip("Sort %s", g_file_sort_ascending ? "Z to A" : "A to Z");
    const bool show_path = !show_view_all || !g_view_all_files;
    const auto button_width = [](const char* label) {
        return ImGui::CalcTextSize(label).x + ImGui::GetStyle().FramePadding.x * 2.0f;
    };
    const char* path_label = ICON_TB_BARS "  Path";
    const char* tree_label = ICON_TB_TREE "  Tree";
    float toggle_width = button_width(tree_label);
    if (show_path) toggle_width += button_width(path_label);
    if (show_path) toggle_width += ImGui::GetStyle().ItemSpacing.x;
    const float row_left = ImGui::GetCursorPosX();
    const float row_width = ImGui::GetContentRegionAvail().x;
    const float toggle_x = row_left + std::max(0.0f, (row_width - toggle_width) * 0.5f);
    ImGui::SameLine(std::max(ImGui::GetCursorPosX() + ui(8), toggle_x));
    if (show_path) {
        const bool path_selected = g_file_view_mode == FileViewMode::path;
        if (path_selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.28f, 0.46f, 1));
        if (ImGui::Button(path_label)) g_file_view_mode = FileViewMode::path;
        if (path_selected) ImGui::PopStyleColor();
        ImGui::SameLine(0, 0);
    }
    const bool tree_selected = g_file_view_mode == FileViewMode::tree;
    if (tree_selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.28f, 0.46f, 1));
    if (ImGui::Button(tree_label)) g_file_view_mode = FileViewMode::tree;
    if (tree_selected) ImGui::PopStyleColor();
    if (show_view_all) {
        ImGui::SameLine();
        if (text_height_checkbox("View all files", &g_view_all_files) && g_view_all_files)
            g_file_view_mode = FileViewMode::tree;
    }
}

bool file_path_less(std::string_view left, std::string_view right) {
    const bool folded_less = std::lexicographical_compare(left.begin(), left.end(), right.begin(), right.end(),
        [](unsigned char lhs, unsigned char rhs) { return std::tolower(lhs) < std::tolower(rhs); });
    const bool folded_greater = std::lexicographical_compare(right.begin(), right.end(), left.begin(), left.end(),
        [](unsigned char lhs, unsigned char rhs) { return std::tolower(lhs) < std::tolower(rhs); });
    return folded_less || (!folded_greater && left < right);
}

template <typename File>
void draw_files(const std::vector<File>& files, bool staged_filter = false,
        bool filter_by_stage = false, const std::string& commit_id = {}) {
    // Git actions reload repository data immediately, so render from a stable snapshot.
    std::vector<File> snapshot = files;
    std::stable_sort(snapshot.begin(), snapshot.end(), [](const File& left, const File& right) {
        return g_file_sort_ascending ? file_path_less(left.path, right.path) : file_path_less(right.path, left.path);
    });
    if (g_file_view_mode == FileViewMode::path) {
        int id = 0;
        for (const auto& file : snapshot) {
            if constexpr (requires { file.staged; }) if (filter_by_stage && file.staged != staged_filter) continue;
            if constexpr (requires { file.staged; }) draw_file_row(file.path, file.kind, id++, true, file.staged);
            else draw_file_row(file.path, file.kind, id++, false, false, {}, commit_id);
        }
        return;
    }
    const auto group_compare = [](const std::string& left, const std::string& right) {
        return g_file_sort_ascending ? file_path_less(left, right) : file_path_less(right, left);
    };
    std::map<std::string, std::vector<const File*>, decltype(group_compare)> groups(group_compare);
    for (const auto& file : snapshot) {
        if constexpr (requires { file.staged; }) if (filter_by_stage && file.staged != staged_filter) continue;
        const size_t slash = file.path.find('/');
        const std::string group = slash == std::string::npos ? "Files" : file.path.substr(0, slash);
        groups[group].push_back(&file);
    }
    int id = 0;
    for (const auto& [group, entries] : groups) {
        if (ImGui::TreeNodeEx((std::string(ICON_TB_FOLDER) + "  " + group).c_str(), ImGuiTreeNodeFlags_DefaultOpen)) {
            ImGui::Indent(ui(12.0f));
            for (const File* file : entries) {
                const size_t slash = file->path.find('/');
                const std::string display = slash == std::string::npos ? file->path : file->path.substr(slash + 1);
                if constexpr (requires { file->staged; })
                    draw_file_row(display, file->kind, id++, true, file->staged, file->path);
                else draw_file_row(display, file->kind, id++, false, false, file->path, commit_id);
            }
            ImGui::Unindent(ui(12.0f));
            ImGui::TreePop();
        }
    }
}

void draw_working_details() {
    int staged = 0;
    for (const auto& file : repo().working_files) if (file.staged) ++staged;
    const int unstaged = static_cast<int>(repo().working_files.size()) - staged;

    ImGui::BeginChild("working_header", {-1, ui(34.0f)}, ImGuiChildFlags_None,
        ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
    ImGui::SetCursorPos({0.0f, ui(5.0f)});
    const bool discard_clicked = ImGui::InvisibleButton("##discard_all", {ui(23.0f), ui(23.0f)});
    const ImVec2 discard_minimum = ImGui::GetItemRectMin();
    const ImVec2 discard_maximum = ImGui::GetItemRectMax();
    ImDrawList* header_draw = ImGui::GetWindowDrawList();
    header_draw->AddRectFilled(discard_minimum, discard_maximum,
        ImGui::IsItemHovered() ? IM_COL32(97, 33, 36, 255) : IM_COL32(69, 26, 28, 255), ui(3.0f));
    const ImVec2 trash_size = ImGui::CalcTextSize(ICON_TB_TRASH_CAN);
    header_draw->AddText({
        discard_minimum.x + (discard_maximum.x - discard_minimum.x - trash_size.x) * 0.5f,
        discard_minimum.y + (discard_maximum.y - discard_minimum.y - trash_size.y) * 0.5f - ui(1.0f),
    }, IM_COL32(224, 92, 92, 255), ICON_TB_TRASH_CAN);
    if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) ImGui::SetTooltip("Discard all changes");
    if (discard_clicked) g_discard_all_popup = true;

    const std::string changes_label = std::to_string(repo().working_files.size()) + " file changes on";
    const float branch_width = ImGui::CalcTextSize(repo().branch.c_str()).x + ui(10.0f);
    const float heading_width = ImGui::CalcTextSize(changes_label.c_str()).x + ui(6.0f) + branch_width;
    ImGui::SameLine(std::max(ImGui::GetCursorPosX() + ui(6.0f),
        (ImGui::GetWindowWidth() - heading_width) * 0.5f));
    ImGui::AlignTextToFramePadding();
    ImGui::TextUnformatted(changes_label.c_str());
    ImGui::SameLine(0, ui(6.0f));
    ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.06f, 0.37f, 0.43f, 1.0f));
    ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.08f, 0.44f, 0.51f, 1.0f));
    ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {ui(5.0f), ui(1.0f)});
    if (ImGui::Button((repo().branch + "##working_branch").c_str()))
        g_request_branch_selector = true;
    ImGui::PopStyleVar();
    ImGui::PopStyleColor(2);

    ImGui::EndChild();
    ImGui::Separator();

    ImGui::BeginChild("working_file_toolbar", {-1, ui(42.0f)}, ImGuiChildFlags_None,
        ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
    ImGui::SetCursorPosY(ui(8.0f));
    draw_file_toolbar(false);
    ImGui::EndChild();

    const float split_height = ImGui::GetContentRegionAvail().y;
    const float minimum_files_height = ui(140.0f);
    const float minimum_composer_height = ui(180.0f);
    const float maximum_composer_height = std::max(minimum_composer_height, split_height - minimum_files_height);
    g_working_composer_height = std::clamp(g_working_composer_height, minimum_composer_height, maximum_composer_height);
    const float composer_height = g_working_composer_height;
    const float files_height = std::max(minimum_files_height, split_height - composer_height);
    ImGui::BeginChild("working_files", {-1, files_height}, ImGuiChildFlags_None,
        ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
    static bool unstaged_open = true;
    static bool staged_open = true;
    const float collapsed_height = ui(27.0f);
    float unstaged_height = files_height * 0.52f;
    float staged_height = files_height - unstaged_height;
    if (!unstaged_open) {
        unstaged_height = collapsed_height;
        staged_height = staged_open ? files_height - collapsed_height : collapsed_height;
    } else if (!staged_open) {
        staged_height = collapsed_height;
        unstaged_height = files_height - collapsed_height;
    }

    ImGui::BeginChild("unstaged_files", {-1, unstaged_height}, ImGuiChildFlags_None);
    const ImVec2 unstaged_header = ImGui::GetCursorScreenPos();
    const std::string unstaged_label =
        std::string(unstaged_open ? ICON_TB_CHEVRON_DOWN "  " : ICON_TB_CHEVRON_RIGHT "  ") +
        "Unstaged Files (" + std::to_string(unstaged) + ")";
    file_section_toggle("##unstaged_toggle", unstaged_label);
    const bool unstaged_header_clicked = ImGui::IsItemClicked();
    ImGui::GetWindowDrawList()->AddText(
        {unstaged_header.x + ui(4.0f), unstaged_header.y + ui(4.0f)},
        ImGui::GetColorU32(ImGuiCol_Text),
        unstaged_label.c_str());
    const float stage_all_width = file_action_button_width("Stage All Changes");
    ImGui::SetCursorScreenPos({ImGui::GetWindowPos().x + ImGui::GetContentRegionMax().x -
            stage_all_width - ui(5.0f),
        unstaged_header.y + ui(2.0f)});
    ImGui::BeginDisabled(unstaged == 0);
    const bool stage_all_clicked = file_action_button("Stage All Changes", true);
    ImGui::EndDisabled();
    if (stage_all_clicked) g_git_manager->stageAll(repo(), g_notice);
    else if (unstaged_header_clicked) unstaged_open = !unstaged_open;
    ImGui::SetCursorScreenPos({unstaged_header.x, unstaged_header.y + ui(24.0f)});
    ImGui::Separator();
    if (unstaged_open) {
        ImGui::BeginChild("unstaged_files_body", {-1, -1}, ImGuiChildFlags_None);
        draw_files(repo().working_files, false, true);
        ImGui::EndChild();
    }
    ImGui::EndChild();

    ImGui::BeginChild("staged_files", {-1, staged_height}, ImGuiChildFlags_None);
    const ImVec2 staged_header = ImGui::GetCursorScreenPos();
    const std::string staged_label =
        std::string(staged_open ? ICON_TB_CHEVRON_DOWN "  " : ICON_TB_CHEVRON_RIGHT "  ") +
        "Staged Files (" + std::to_string(staged) + ")";
    file_section_toggle("##staged_toggle", staged_label);
    const bool staged_header_clicked = ImGui::IsItemClicked();
    ImGui::GetWindowDrawList()->AddText(
        {staged_header.x + ui(4.0f), staged_header.y + ui(4.0f)},
        ImGui::GetColorU32(ImGuiCol_Text),
        staged_label.c_str());
    const float unstage_all_width = file_action_button_width("Unstage All Changes");
    ImGui::SetCursorScreenPos({ImGui::GetWindowPos().x + ImGui::GetContentRegionMax().x -
            unstage_all_width - ui(5.0f), staged_header.y + ui(2.0f)});
    ImGui::BeginDisabled(staged == 0);
    const bool unstage_all_clicked = file_action_button("Unstage All Changes", false);
    ImGui::EndDisabled();
    if (unstage_all_clicked) g_git_manager->unstageAll(repo(), g_notice);
    else if (staged_header_clicked) staged_open = !staged_open;
    ImGui::SetCursorScreenPos({staged_header.x, staged_header.y + ui(24.0f)});
    ImGui::Separator();
    if (staged_open) {
        ImGui::BeginChild("staged_files_body", {-1, -1}, ImGuiChildFlags_None);
        draw_files(repo().working_files, true, true);
        ImGui::EndChild();
    }
    ImGui::EndChild();
    ImGui::EndChild();

    ImGui::BeginChild("commit_composer", {-1, -1}, ImGuiChildFlags_None,
        ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
    const float handle_x = (ImGui::GetWindowWidth() - ui(90.0f)) * 0.5f;
    const ImVec2 handle = ImGui::GetCursorScreenPos();
    ImGui::SetCursorPosX(handle_x);
    ImGui::InvisibleButton("##working_composer_resize", {ui(90.0f), ui(10.0f)});
    const bool composer_resize_active = ImGui::IsItemActive();
    const bool composer_resize_hovered = ImGui::IsItemHovered();
    if (composer_resize_active || composer_resize_hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS);
    if (composer_resize_active) {
        g_working_composer_height = std::clamp(
            g_working_composer_height + ImGui::GetIO().MouseDelta.y,
            minimum_composer_height, maximum_composer_height);
    }
    ImGui::GetWindowDrawList()->AddLine(
        {handle.x + handle_x, handle.y + ui(3.0f)},
        {handle.x + handle_x + ui(90.0f), handle.y + ui(3.0f)},
        composer_resize_active || composer_resize_hovered
            ? IM_COL32(23, 181, 204, 255) : IM_COL32(56, 59, 65, 255),
        ui(3.0f));
    ImGui::Dummy({0.0f, ui(10.0f)});
    ImGui::TextUnformatted(ICON_TB_CIRCLE_DOT "  Commit");
    ImGui::Separator();
    ImGui::Indent(ui(8.0f));

    static bool amend = false;
    text_height_checkbox("Amend previous commit", &amend);

    ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(22 / 255.0f, 25 / 255.0f, 30 / 255.0f, 1.0f));
    ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(70 / 255.0f, 77 / 255.0f, 89 / 255.0f, 1.0f));
    ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, ui(1.0f));
    ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, ui(4.0f));
    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {ui(8.0f), ui(7.0f)});
    ImGui::BeginChild("commit_message_card", {-1, ui(165.0f)},
        ImGuiChildFlags_Borders | ImGuiChildFlags_AlwaysUseWindowPadding);
    ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(15 / 255.0f, 18 / 255.0f, 22 / 255.0f, 1.0f));
    ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(15 / 255.0f, 18 / 255.0f, 22 / 255.0f, 1.0f));
    ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(15 / 255.0f, 18 / 255.0f, 22 / 255.0f, 1.0f));
    ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
    ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, ui(3.0f));
    const float summary_controls_width = ui(30.0f);
    const int commit_summary_recommendation = 72;
    const int commit_summary_length = static_cast<int>(std::strlen(g_commit_summary.data()));
    const int commit_summary_remaining = commit_summary_recommendation - commit_summary_length;
    ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - summary_controls_width);
    ImGui::InputTextWithHint("##commit_summary", "Commit summary", g_commit_summary.data(), g_commit_summary.size());
    ImGui::SameLine();
    const ImVec4 summary_counter_color = commit_summary_remaining >= 0
        ? ImVec4(0.60f, 0.62f, 0.66f, 1.0f)
        : commit_summary_remaining > -10
            ? ImVec4(0.92f, 0.76f, 0.31f, 1.0f)
            : ImVec4(0.92f, 0.42f, 0.42f, 1.0f);
    ImGui::TextColored(summary_counter_color, "%d", commit_summary_remaining);
    ImGui::SetNextItemWidth(-1);
    ImGui::InputTextMultiline("##commit_description", g_commit_description.data(), g_commit_description.size(),
        {-1, -1});
    if (g_commit_description[0] == '\0' && !ImGui::IsItemActive()) {
        const ImVec2 description_minimum = ImGui::GetItemRectMin();
        ImGui::GetWindowDrawList()->AddText(
            {description_minimum.x + ui(6.0f), description_minimum.y + ui(5.0f)},
            IM_COL32(154, 159, 168, 255), "Description");
    }
    ImGui::PopStyleVar(2);
    ImGui::PopStyleColor(3);
    ImGui::EndChild();
    ImGui::PopStyleVar(3);
    ImGui::PopStyleColor(2);

    ImGui::TextDisabled(ICON_TB_CHEVRON_RIGHT "  Commit options");

    ImGui::BeginDisabled(staged == 0 || g_commit_summary[0] == '\0');
    ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.12f, 0.24f, 0.18f, 1.0f));
    ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.24f, 0.54f, 0.32f, 1.0f));
    if (ImGui::Button(ICON_TB_CIRCLE_DOT "  Stage Changes to Commit", {-1, ui(40.0f)})) {
        if (g_git_manager->commit(repo(), g_commit_summary.data(), g_commit_description.data(), amend, g_notice)) {
            g_commit_summary.fill('\0');
            g_commit_description.fill('\0');
            amend = false;
        }
    }
    ImGui::PopStyleColor(2);
    ImGui::EndDisabled();
    ImGui::Unindent(ui(8.0f));
    ImGui::EndChild();
}

void draw_commit_details() {
    const int selected = std::clamp(repo().selected_commit, 0, static_cast<int>(repo().commits.size() - 1));
    g_git_manager->loadCommitChanges(repo(), selected, g_notice);
    auto& commit = repo().commits[selected];
    if (!repo().working_files.empty()) {
        const std::string changes = std::to_string(repo().working_files.size()) +
            (repo().working_files.size() == 1
                ? " file change in working directory"
                : " file changes in working directory");
        ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(43 / 255.0f, 72 / 255.0f, 139 / 255.0f, 1.0f));
        ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {ui(9.0f), ui(5.0f)});
        ImGui::BeginChild("working_changes_banner", {-1, ui(34.0f)},
            ImGuiChildFlags_AlwaysUseWindowPadding,
            ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
        ImGui::PopStyleVar();
        ImGui::AlignTextToFramePadding();
        ImGui::TextUnformatted(changes.c_str());
        const char* view_label = "View Changes";
        const float button_width = ImGui::CalcTextSize(view_label).x + ImGui::GetStyle().FramePadding.x * 2.0f;
        ImGui::SameLine(std::max(ImGui::GetCursorPosX() + ui(8.0f),
            ImGui::GetContentRegionMax().x - button_width));
        ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.16f, 0.24f, 0.45f, 1.0f));
        ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.20f, 0.31f, 0.58f, 1.0f));
        if (ImGui::Button(view_label)) repo().selected_commit = -1;
        ImGui::PopStyleColor(2);
        ImGui::EndChild();
        ImGui::PopStyleColor();
        ImGui::Dummy({0.0f, ui(5.0f)});
    }
    ImGui::TextDisabled("commit:");
    ImGui::SameLine();
    const std::string commit_hash = oid_string(commit.oid);
    draw_copyable_hash(commit.short_id, commit_hash);
    ImGui::Separator();
    const float maximum_message_height = std::clamp(
        ImGui::GetContentRegionAvail().y / g_ui_scale - 190.0f, 72.0f, 320.0f);
    g_commit_message_height = std::min(g_commit_message_height, maximum_message_height);
    ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(22 / 255.0f, 25 / 255.0f, 30 / 255.0f, 1.0f));
    ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(70 / 255.0f, 77 / 255.0f, 89 / 255.0f, 1.0f));
    ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, ui(1.0f));
    ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, ui(4.0f));
    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {ui(8.0f), ui(7.0f)});
    ImGui::BeginChild("message_card", {-1, ui(g_commit_message_height)},
        ImGuiChildFlags_Borders | ImGuiChildFlags_AlwaysUseWindowPadding);
    ImGui::PushTextWrapPos();
    ImGui::Text("%s", commit.summary.c_str());
    ImGui::PopTextWrapPos();
    ImGui::EndChild();
    ImGui::PopStyleVar(3);
    ImGui::PopStyleColor(2);
    ImGui::SetCursorPosX((ImGui::GetWindowWidth() - ui(90.0f)) * 0.5f);
    ImGui::InvisibleButton("##commit_message_resize", {ui(90.0f), ui(10.0f)});
    const bool resize_active = ImGui::IsItemActive();
    const bool resize_hovered = ImGui::IsItemHovered();
    if (resize_active || resize_hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS);
    if (resize_active) {
        g_commit_message_height = std::clamp(
            g_commit_message_height + ImGui::GetIO().MouseDelta.y / g_ui_scale,
            72.0f, maximum_message_height);
    }
    const ImVec2 handle_minimum = ImGui::GetItemRectMin();
    const ImVec2 handle_maximum = ImGui::GetItemRectMax();
    const float handle_y = (handle_minimum.y + handle_maximum.y) * 0.5f;
    ImGui::GetWindowDrawList()->AddLine(
        {handle_minimum.x, handle_y}, {handle_maximum.x, handle_y},
        resize_active || resize_hovered ? IM_COL32(23, 181, 204, 255) : IM_COL32(72, 80, 94, 255),
        ui(3.0f));

    const unsigned int author_texture = g_avatar_cache ? g_avatar_cache->textureFor(commit.email) : 0;
    if (author_texture) {
        const ImVec2 avatar_size{ui(40.0f), ui(40.0f)};
        ImGui::InvisibleButton("##author_avatar", avatar_size);
        const ImVec2 avatar_minimum = ImGui::GetItemRectMin();
        ImGui::GetWindowDrawList()->AddImageRounded(
            ImTextureRef(static_cast<ImTextureID>(author_texture)),
            avatar_minimum, {avatar_minimum.x + avatar_size.x, avatar_minimum.y + avatar_size.y},
            {0, 0}, {1, 1}, IM_COL32_WHITE, ui(6.0f));
        ImGui::SameLine();
    }
    ImGui::BeginGroup();
    ImGui::TextColored(ImVec4(0.25f, 0.80f, 0.86f, 1), "%s", commit.author.c_str());
    ImGui::TextDisabled("authored %s", commit.date.c_str());
    ImGui::EndGroup();
    if (!commit.parent_ids.empty()) {
        const std::string parent_hash = oid_string(commit.parent_ids.front());
        const float parent_width = ImGui::CalcTextSize("parent:").x + ui(4.0f) +
            ImGui::CalcTextSize(parent_hash.substr(0, 7).c_str()).x;
        ImGui::SameLine(std::max(ImGui::GetCursorPosX() + ui(8.0f),
            ImGui::GetContentRegionMax().x - parent_width - ui(5.0f)));
        ImGui::TextDisabled("parent:");
        ImGui::SameLine(0, ui(4.0f));
        draw_jumpable_hash(std::string_view(parent_hash).substr(0, 7), parent_hash);
    }
    ImGui::Dummy({0, ui(14)});
    int added = 0, modified = 0, deleted = 0;
    for (const auto& file : commit.changed_files) {
        if (file.kind == FileChangeKind::added) ++added;
        else if (file.kind == FileChangeKind::deleted) ++deleted;
        else ++modified;
    }
    if (modified) ImGui::TextColored(change_color(FileChangeKind::modified), ICON_TB_PEN " %d modified", modified);
    if (added) { if (modified) ImGui::SameLine(); ImGui::TextColored(change_color(FileChangeKind::added), ICON_TB_PLUS " %d added", added); }
    if (deleted) { if (modified || added) ImGui::SameLine(); ImGui::TextColored(change_color(FileChangeKind::deleted), ICON_TB_MINUS " %d deleted", deleted); }
    draw_file_toolbar(true);
    ImGui::Separator();
    if (g_file_view_mode == FileViewMode::tree) ImGui::TextUnformatted("Collapse All");
    ImGui::BeginChild("changed_files", {-1, -1});
    if (g_view_all_files) g_git_manager->loadCommitFiles(repo(), selected, g_notice);
    const auto& displayed_files = g_view_all_files && commit.all_files_loaded
        ? commit.all_files : commit.changed_files;
    draw_files(displayed_files, false, false, oid_string(commit.oid));
    ImGui::EndChild();
}

void draw_details(float width) {
    ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(33 / 255.0f, 36 / 255.0f, 43 / 255.0f, 1.0f));
    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {ui(15.0f), ui(8.0f)});
    if (g_reset_repository_view) ImGui::SetNextWindowScroll({0.0f, 0.0f});
    ImGui::BeginChild("details", {width, -ui(28.0f)}, ImGuiChildFlags_None);
    ImGui::PopStyleVar();
    if (repo().selected_commit == -1) draw_working_details();
    else if (!repo().commits.empty()) draw_commit_details();
    else ImGui::TextDisabled("Select a repository with commits");
    ImGui::EndChild();
    ImGui::PopStyleColor();
}

void draw_toolbar_spinner(ImDrawList* draw, const ImVec2& center, float radius, ImU32 color) {
    const float start = static_cast<float>(ImGui::GetTime() * 6.0);
    const float span = 4.4f;
    constexpr int segments = 24;
    draw->PathClear();
    for (int index = 0; index < segments; ++index) {
        const float t = static_cast<float>(index) / static_cast<float>(segments - 1);
        const float angle = start + span * t;
        draw->PathLineTo({center.x + std::cos(angle) * radius, center.y + std::sin(angle) * radius});
    }
    draw->PathStroke(color, false, ui(2.0f));
}

bool toolbar_action(const char* id, const char* label, const char* icon, const char* tooltip,
        bool enabled = true, bool dropdown = false, float logical_width = 58.0f, bool spinning = false,
        bool* dropdown_clicked = nullptr) {
    ImGui::PushID(id);
    if (!enabled) ImGui::BeginDisabled();
    const bool raw_clicked = ImGui::InvisibleButton("##action", {ui(logical_width), ui(46.0f)});
    const ImVec2 minimum = ImGui::GetItemRectMin();
    const ImVec2 maximum = ImGui::GetItemRectMax();
    const bool clicked_dropdown = raw_clicked && dropdown &&
        ImGui::GetIO().MousePos.x >= maximum.x - ui(20.0f);
    if (dropdown_clicked) *dropdown_clicked = clicked_dropdown;
    const bool clicked = raw_clicked && !clicked_dropdown;
    ImDrawList* draw = ImGui::GetWindowDrawList();
    if (enabled && ImGui::IsItemHovered())
        draw->AddRectFilled(minimum, maximum, IM_COL32(62, 66, 75, 210));
    const ImU32 text_color = enabled ? IM_COL32(218, 221, 226, 255) : IM_COL32(139, 144, 153, 255);
    const float label_font_size = ImGui::GetFontSize() * 0.62f;
    const float icon_font_size = ImGui::GetFontSize() * 1.34f;
    const ImVec2 label_size = g_regular_font
        ? g_regular_font->CalcTextSizeA(label_font_size, std::numeric_limits<float>::max(), 0.0f, label)
        : ImGui::CalcTextSize(label);
    const ImVec2 icon_size = g_regular_font
        ? g_regular_font->CalcTextSizeA(icon_font_size, std::numeric_limits<float>::max(), 0.0f, icon)
        : ImGui::CalcTextSize(icon);
    draw->AddText(g_regular_font, label_font_size,
        {minimum.x + (maximum.x - minimum.x - label_size.x) * 0.5f, minimum.y + ui(4.0f)},
        text_color, label);
    const float icon_x = minimum.x + (maximum.x - minimum.x - icon_size.x) * 0.5f - (dropdown ? ui(4.0f) : 0.0f);
    if (spinning) {
        const ImVec2 center{
            minimum.x + (maximum.x - minimum.x) * 0.5f - (dropdown ? ui(4.0f) : 0.0f),
            minimum.y + ui(27.0f)
        };
        draw_toolbar_spinner(draw, center, ui(7.0f), text_color);
    } else {
        draw->AddText(g_regular_font, icon_font_size, {icon_x, minimum.y + ui(19.0f)}, text_color, icon);
    }
    if (dropdown)
        draw->AddText({icon_x + icon_size.x + ui(7.0f), minimum.y + ui(21.0f)}, text_color, ICON_TB_CARET_DOWN);
    if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) {
        ImGui::BeginTooltip();
        ImGui::TextUnformatted(tooltip);
        ImGui::EndTooltip();
    }
    if (!enabled) ImGui::EndDisabled();
    ImGui::PopID();
    return clicked;
}

const char* pull_mode_name(int mode) {
    constexpr const char* names[] = {
        "Fetch All",
        "Pull (fast-forward if possible)",
        "Pull (fast-forward only)",
        "Pull (rebase)",
    };
    return names[std::clamp(mode, 0, 3)];
}

void draw_pull_options() {
    ImGui::SetNextWindowSize({ui(270), 0});
    if (!ImGui::BeginPopup("pull_options")) return;

    ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + ui(245));
    ImGui::TextDisabled("Select a default pull/fetch operation to execute when clicking this button");
    ImGui::PopTextWrapPos();
    ImGui::Separator();

    int selected = g_user_data->pullMode();
    for (int mode = 0; mode < 4; ++mode) {
        ImGui::PushID(mode);
        if (ImGui::RadioButton(pull_mode_name(mode), selected == mode)) {
            g_user_data->setPullMode(mode);
            g_notice = std::string("Default action: ") + pull_mode_name(mode);
            ImGui::CloseCurrentPopup();
        }
        ImGui::PopID();
    }
    ImGui::EndPopup();
}

bool toolbar_selector(const char* id, const char* label, const std::string& value,
        float logical_width, bool trailing_arrow = false) {
    ImGui::PushID(id);
    const bool clicked = ImGui::InvisibleButton("##selector", {ui(logical_width), ui(46.0f)});
    const ImVec2 minimum = ImGui::GetItemRectMin();
    const ImVec2 maximum = ImGui::GetItemRectMax();
    ImDrawList* draw = ImGui::GetWindowDrawList();
    const bool hovered = ImGui::IsItemHovered();
    if (hovered) draw->AddRectFilled(minimum, maximum, IM_COL32(62, 66, 75, 200));
    const float label_font_size = ImGui::GetFontSize() * 0.78f;
    draw->AddText(g_regular_font, label_font_size,
        {minimum.x + ui(8), minimum.y + ui(4.0f)}, IM_COL32(166, 172, 182, 255), label);
    const float value_width = maximum.x - minimum.x - ui(34.0f);
    std::string displayed = value;
    if (ImGui::CalcTextSize(displayed.c_str()).x > value_width) {
        constexpr const char* ellipsis = "...";
        const float ellipsis_width = ImGui::CalcTextSize(ellipsis).x;
        while (!displayed.empty() && ImGui::CalcTextSize(displayed.c_str()).x + ellipsis_width > value_width) {
            size_t character_start = displayed.size() - 1;
            while (character_start > 0 &&
                    (static_cast<unsigned char>(displayed[character_start]) & 0xC0) == 0x80)
                --character_start;
            displayed.erase(character_start);
        }
        displayed += ellipsis;
    }
    draw->AddText({minimum.x + ui(8), minimum.y + ui(25.0f)}, IM_COL32(226, 229, 233, 255), displayed.c_str());
    draw->AddText({maximum.x - ui(18), minimum.y + ui(25.0f)}, IM_COL32(160, 165, 174, 255),
        trailing_arrow ? ICON_TB_ANGLE_RIGHT : ICON_TB_CARET_DOWN);
    if (displayed != value && ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort))
        ImGui::SetTooltip("%s", value.c_str());
    ImGui::PopID();
    return clicked;
}

const char* application_icon(ExternalApplicationId application) {
    switch (application) {
        case ExternalApplicationId::visual_studio_code: return ICON_TB_CODE;
        case ExternalApplicationId::visual_studio: return ICON_TB_CUBES;
        case ExternalApplicationId::antigravity: return ICON_TB_WINDOW_MAXIMIZE;
        case ExternalApplicationId::github_desktop: return ICON_TB_CIRCLE_NODES;
        case ExternalApplicationId::file_explorer: return ICON_TB_FOLDER;
        case ExternalApplicationId::terminal: return ICON_TB_TERMINAL;
        case ExternalApplicationId::git_bash: return ICON_TB_CODE_BRANCH;
        case ExternalApplicationId::wsl: return ICON_TB_SERVER;
        case ExternalApplicationId::android_studio: return ICON_TB_ROBOT;
        case ExternalApplicationId::intellij_idea: return ICON_TB_JET_FIGHTER_UP;
        default: return ICON_TB_ARROW_UP_RIGHT_FROM_SQUARE;
    }
}

unsigned int application_icon_texture(ExternalApplicationId application) {
    if (!g_application_manager || !g_application_icon_cache) return 0;
    const ExternalApplication* details = g_application_manager->application(application);
    return details ? g_application_icon_cache->textureFor(details->executable) : 0;
}

void open_repository_in(ExternalApplicationId application) {
    if (!g_application_manager) return;
    g_open_in_application = application;
    g_application_manager->launch(application, repo().path, g_notice);
}

void draw_open_in_button() {
    if (!g_application_manager) return;
    constexpr float button_width = 76.0f;
    constexpr float primary_width = 52.0f;
    constexpr float menu_width = button_width - primary_width;
    ImGui::SetCursorPos({ImGui::GetWindowWidth() - ui(button_width + 7.0f), ui(10.0f)});
    const ImVec2 open_button_size{ui(button_width), ui(30.0f)};
    ImGui::InvisibleButton("##open_in", open_button_size);
    const bool open_hovered = ImGui::IsItemHovered();
    const bool open_clicked = ImGui::IsItemClicked();
    const ImVec2 open_minimum = ImGui::GetItemRectMin();
    const ImVec2 open_maximum = ImGui::GetItemRectMax();
    const float divider_x = open_minimum.x + ui(primary_width);
    const bool menu_segment = open_hovered && ImGui::GetIO().MousePos.x >= divider_x;
    ImDrawList* open_draw = ImGui::GetWindowDrawList();
    const float rounding = ui(7.0f);
    open_draw->AddRectFilled(open_minimum, open_maximum, IM_COL32(28, 32, 38, 255), rounding);
    if (open_hovered) {
        if (menu_segment)
            open_draw->AddRectFilled({divider_x, open_minimum.y}, open_maximum,
                IM_COL32(45, 51, 60, 255), rounding, ImDrawFlags_RoundCornersRight);
        else
            open_draw->AddRectFilled(open_minimum, {divider_x, open_maximum.y},
                IM_COL32(45, 51, 60, 255), rounding, ImDrawFlags_RoundCornersLeft);
    }
    open_draw->AddRect(open_minimum, open_maximum, IM_COL32(56, 62, 72, 255), rounding, 0, ui(1.0f));
    const unsigned int selected_texture = application_icon_texture(g_open_in_application);
    if (selected_texture) {
        const float icon_size = ui(18.0f);
        const ImVec2 icon_minimum{
            open_minimum.x + (ui(primary_width) - icon_size) * 0.5f,
            open_minimum.y + (open_button_size.y - icon_size) * 0.5f,
        };
        open_draw->AddImage(ImTextureRef(static_cast<ImTextureID>(selected_texture)), icon_minimum,
            {icon_minimum.x + icon_size, icon_minimum.y + icon_size});
    } else {
        const char* fallback = application_icon(g_open_in_application);
        const ImVec2 icon_size = ImGui::CalcTextSize(fallback);
        open_draw->AddText({open_minimum.x + (ui(primary_width) - icon_size.x) * 0.5f,
            open_minimum.y + (open_button_size.y - icon_size.y) * 0.5f},
            IM_COL32(51, 173, 242, 255), fallback);
    }
    const ImVec2 chevron_size = ImGui::CalcTextSize(ICON_TB_CHEVRON_DOWN);
    open_draw->AddText({divider_x + (ui(menu_width) - chevron_size.x) * 0.5f,
        open_minimum.y + (open_button_size.y - chevron_size.y) * 0.5f - ui(1.5f)},
        IM_COL32(177, 184, 194, 255), ICON_TB_CHEVRON_DOWN);
    if (open_clicked) {
        if (menu_segment) ImGui::OpenPopup("open_repository_in");
        else open_repository_in(g_open_in_application);
    }
    if (open_hovered && ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) {
        const ExternalApplication* application = g_application_manager->application(g_open_in_application);
        if (menu_segment) ImGui::SetTooltip("Choose application");
        else ImGui::SetTooltip("Open in %s", application ? application->name.c_str() : "application");
    }

    const int available_applications = static_cast<int>(std::count_if(
        g_application_manager->applications().begin(), g_application_manager->applications().end(),
        [](const ExternalApplication& application) { return application.available; }));
    const float popup_width = ui(200.0f);
    const float popup_height = ui(available_applications > 0
        ? 12.0f + available_applications * 28.0f : 48.0f);
    ImGui::SetNextWindowSize({popup_width, popup_height}, ImGuiCond_Always);
    ImGui::SetNextWindowPos(
        {open_maximum.x - popup_width, open_maximum.y + ui(4.0f)}, ImGuiCond_Always);
    ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, ui(8.0f));
    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {ui(6.0f), ui(6.0f)});
    const bool popup_open = ImGui::BeginPopup("open_repository_in");
    ImGui::PopStyleVar(2);
    if (popup_open) {
        if (available_applications == 0) {
            ImGui::TextDisabled("No applications found");
        }
        for (const ExternalApplication& application : g_application_manager->applications()) {
            if (!application.available) continue;
            ImGui::PushID(static_cast<int>(application.id));
            const ImVec2 row_minimum = ImGui::GetCursorScreenPos();
            const bool selected = application.id == g_open_in_application;
            const bool clicked = ImGui::Selectable("##application", selected,
                ImGuiSelectableFlags_None, {popup_width - ui(12.0f), ui(28.0f)});
            const ImVec2 row_maximum = ImGui::GetItemRectMax();
            const unsigned int texture = application_icon_texture(application.id);
            if (texture) {
                const float icon_size = ui(18.0f);
                const ImVec2 icon_minimum{row_minimum.x + ui(5.0f),
                    row_minimum.y + (row_maximum.y - row_minimum.y - icon_size) * 0.5f};
                ImGui::GetWindowDrawList()->AddImage(
                    ImTextureRef(static_cast<ImTextureID>(texture)), icon_minimum,
                    {icon_minimum.x + icon_size, icon_minimum.y + icon_size});
            }
            const float text_x = row_minimum.x + ui(31.0f);
            const float text_y = row_minimum.y +
                (row_maximum.y - row_minimum.y - ImGui::GetFontSize()) * 0.5f;
            ImGui::GetWindowDrawList()->AddText({text_x, text_y},
                IM_COL32(226, 230, 235, 255), application.name.c_str());
            if (clicked) {
                open_repository_in(application.id);
                ImGui::CloseCurrentPopup();
            }
            ImGui::PopID();
        }
        ImGui::EndPopup();
    }
}

bool contains_case_insensitive(const std::string& text, const char* query) {
    if (!query || !*query) return true;
    std::string lowered_text = text;
    std::string lowered_query = query;
    const auto lower = [](unsigned char character) { return static_cast<char>(std::tolower(character)); };
    std::transform(lowered_text.begin(), lowered_text.end(), lowered_text.begin(), lower);
    std::transform(lowered_query.begin(), lowered_query.end(), lowered_query.begin(), lower);
    return lowered_text.find(lowered_query) != std::string::npos;
}

void external_link(const char* label, const char* url) {
    ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.31f, 0.76f, 0.92f, 1.0f));
    if (ImGui::Selectable(label, false, ImGuiSelectableFlags_DontClosePopups)) {
        std::string error;
        if (!izo::OpenPath(url, &error)) g_notice = error.empty() ? "Unable to open link" : error;
    }
    ImGui::PopStyleColor();
    if (ImGui::IsItemHovered()) {
        ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
        ImGui::SetTooltip("%s", url);
    }
}

void draw_about_popup() {
    if (g_about_popup) {
        ImGui::OpenPopup("About Gitree");
        g_about_popup = false;
    }
    if (!ImGui::BeginPopupModal("About Gitree", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) return;

    ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.38f, 0.88f, 0.94f, 1.0f));
    ImGui::PushFont(g_bold_font, 0.0f);
    ImGui::SetWindowFontScale(1.35f);
    ImGui::TextUnformatted("Gitree");
    ImGui::SetWindowFontScale(1.0f);
    ImGui::PopFont();
    ImGui::PopStyleColor();
    ImGui::TextDisabled("Version %s", GITREE_VERSION);
    ImGui::Dummy({ui(460), ui(8)});
    ImGui::TextWrapped("A fast native Git client built with C++, Dear ImGui, GLFW, and libgit2.");
    ImGui::Spacing();
    external_link("Gitree project page", "https://dock-it.dev/iDENTITY-Technology/Gitree");
    ImGui::Separator();
    if (ImGui::Button("Licenses", {ui(110), 0})) {
        g_licenses_popup = true;
        ImGui::CloseCurrentPopup();
    }
    ImGui::SameLine();
    if (ImGui::Button("Close", {ui(90), 0})) ImGui::CloseCurrentPopup();
    ImGui::EndPopup();
}

void draw_licenses_popup() {
    if (g_licenses_popup) {
        ImGui::OpenPopup("Licenses and dependencies");
        g_licenses_popup = false;
    }
    if (!ImGui::BeginPopupModal("Licenses and dependencies", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) return;

    if (ImGui::BeginTable("dependency_licenses", 2, ImGuiTableFlags_SizingFixedFit, {ui(510), 0})) {
        ImGui::TableSetupColumn("##name", ImGuiTableColumnFlags_WidthFixed, ui(210));
        ImGui::TableSetupColumn("##license", ImGuiTableColumnFlags_WidthStretch);
        struct Dependency { const char* name; const char* license; const char* url; };
        constexpr Dependency dependencies[] = {
            {"Gitree", "CC BY-SA 4.0", "https://dock-it.dev/iDENTITY-Technology/Gitree"},
            {"libgit2", "GPL-2.0 with linking exception", "https://github.com/libgit2/libgit2"},
            {"Dear ImGui", "MIT License", "https://github.com/ocornut/imgui"},
            {"GLFW", "zlib/libpng License", "https://github.com/glfw/glfw"},
            {"iZo", "MIT License", "https://dock-it.dev/Idea-Studios/iZo"},
            {"iKv", "CC BY-SA 4.0", "https://dock-it.dev/Idea-Studios/iKv"},
            {"Inter", "SIL Open Font License 1.1", "https://github.com/rsms/inter"},
            {"Tabler Icons", "MIT License", "https://github.com/tabler/tabler-icons"},
        };
        for (const auto& dependency : dependencies) {
            ImGui::TableNextRow();
            ImGui::TableSetColumnIndex(0);
            external_link(dependency.name, dependency.url);
            ImGui::TableSetColumnIndex(1);
            ImGui::TextDisabled("%s", dependency.license);
        }
        ImGui::EndTable();
    }
    ImGui::Spacing();
    if (ImGui::Button("Close", {ui(90), 0})) ImGui::CloseCurrentPopup();
    ImGui::EndPopup();
}

void draw_git_credentials_popup() {
    if (g_git_auth_popup) {
        ImGui::OpenPopup("Git credentials required");
        g_git_auth_popup = false;
    }
    if (!ImGui::BeginPopupModal("Git credentials required", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) return;

    ImGui::TextWrapped("Authentication is required for this remote.");
    if (!g_git_auth_remote_url.empty()) {
        ImGui::Spacing();
        ImGui::TextDisabled("Remote");
        ImGui::TextWrapped("%s", g_git_auth_remote_url.c_str());
    }
    if (!g_git_auth_error.empty()) {
        ImGui::Spacing();
        ImGui::TextColored(ImVec4(0.95f, 0.47f, 0.47f, 1.0f), "%s", g_git_auth_error.c_str());
    }

    ImGui::Spacing();
    ImGui::SetNextItemWidth(ui(320.0f));
    ImGui::InputText("Username", g_git_auth_username.data(), g_git_auth_username.size());
    ImGui::SetNextItemWidth(ui(320.0f));
    ImGui::InputText("Password", g_git_auth_password.data(), g_git_auth_password.size(),
        ImGuiInputTextFlags_Password);
    ImGui::Checkbox("Remember securely", &g_git_auth_remember);

    const bool can_retry = g_git_auth_retry_request.has_value() && g_git_auth_username[0] != '\0';
    if (!can_retry) ImGui::BeginDisabled();
    if (ImGui::Button("Retry", {ui(110.0f), 0.0f})) {
        GitAsyncRequest request = *g_git_auth_retry_request;
        request.auth_override = GitAuthOverride{
            g_git_auth_remote_url,
            g_git_auth_username.data(),
            g_git_auth_password.data(),
        };
        request.used_saved_app_credential = false;
        if (g_git_auth_remember && g_user_data && !g_git_auth_remote_url.empty()) {
            RepositoryView* target = find_repository_tab(request.tab);
            std::string store_error;
            bool stored_in_helper = false;
            if (target && g_git_manager)
                stored_in_helper = g_git_manager->storeCredential(*target, g_git_auth_remote_url,
                    request.auth_override->username, request.auth_override->password, store_error);
            if (stored_in_helper) g_user_data->clearRemoteCredential(g_git_auth_remote_url);
            else g_user_data->storeRemoteCredential(g_git_auth_remote_url,
                request.auth_override->username, request.auth_override->password);
        } else if (g_user_data && !g_git_auth_remote_url.empty()) {
            g_user_data->clearRemoteCredential(g_git_auth_remote_url);
        }
        if (start_git_async_request(std::move(request))) {
            std::fill(g_git_auth_password.begin(), g_git_auth_password.end(), '\0');
            g_git_auth_retry_request.reset();
            ImGui::CloseCurrentPopup();
        }
    }
    if (!can_retry) ImGui::EndDisabled();
    ImGui::SameLine();
    if (ImGui::Button("Cancel", {ui(110.0f), 0.0f})) {
        g_git_auth_retry_request.reset();
        std::fill(g_git_auth_password.begin(), g_git_auth_password.end(), '\0');
        g_notice = "Authentication cancelled";
        ImGui::CloseCurrentPopup();
    }
    ImGui::EndPopup();
}

void draw_git_action_popups() {
    static bool checkout_new_branch = true;
    draw_git_credentials_popup();
    if (g_merge_branch_popup) {
        g_merge_branch_popup = false;
        ImGui::OpenPopup("Merge branches");
    }
    if (ImGui::BeginPopupModal("Merge branches", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
        ImGui::TextUnformatted(ICON_TB_ARROW_RIGHT_ARROW_LEFT "  Merge branches");
        ImGui::Separator();
        ImGui::Spacing();
        ImGui::TextUnformatted(ICON_TB_CODE_BRANCH "  Source branch");
        ImGui::TextDisabled("%s", g_merge_branch_source.c_str());
        ImGui::TextUnformatted(ICON_TB_CODE_BRANCH "  Target branch");
        ImGui::TextDisabled("%s", g_merge_branch_target.c_str());
        if (g_merge_branch_target != repo().branch)
            ImGui::TextWrapped("%s The target branch will be checked out first, then the merge will run on that branch.",
                ICON_TB_TRIANGLE_EXCLAMATION);
        else
            ImGui::TextWrapped("This will merge the source branch into the currently checked out branch.");
        ImGui::Spacing();
        ImGui::BeginDisabled(g_merge_branch_source.empty() || g_merge_branch_target.empty() ||
            g_merge_branch_source == g_merge_branch_target);
        ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.35f, 0.22f, 1.0f));
        ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.18f, 0.42f, 0.27f, 1.0f));
        ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.20f, 0.48f, 0.30f, 1.0f));
        if (ImGui::Button(ICON_TB_ARROW_RIGHT_ARROW_LEFT "  Merge branch", {ui(150.0f), 0}) &&
                g_git_manager->mergeBranch(repo(), g_merge_branch_source, g_merge_branch_target, g_notice)) {
            mark_action_refresh(repo());
            cancel_merge_branch();
            ImGui::CloseCurrentPopup();
        }
        ImGui::PopStyleColor(3);
        ImGui::EndDisabled();
        ImGui::SameLine();
        if (ImGui::Button(ICON_TB_XMARK "  Cancel", {ui(90.0f), 0})) {
            cancel_merge_branch();
            ImGui::CloseCurrentPopup();
        }
        ImGui::EndPopup();
    }

    if (g_branch_create_popup) {
        g_git_name.fill('\0');
        checkout_new_branch = true;
        g_branch_create_popup = false;
        ImGui::OpenPopup("Create local branch");
    }
    if (ImGui::BeginPopupModal("Create local branch", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
        ImGui::TextUnformatted("Branch name");
        ImGui::SetNextItemWidth(ui(380.0f));
        if (ImGui::IsWindowAppearing()) ImGui::SetKeyboardFocusHere();
        const bool submit_with_enter = ImGui::InputText("##local_branch_name",
            g_git_name.data(), g_git_name.size(), ImGuiInputTextFlags_EnterReturnsTrue);
        text_height_checkbox("Check out after creating", &checkout_new_branch);
        ImGui::TextDisabled("Branching off: %s", g_git_target_label.empty()
            ? (repo().branch.empty() ? "HEAD" : repo().branch.c_str())
            : g_git_target_label.c_str());
        ImGui::TextDisabled("Start point: %.12s", g_git_target.empty() ? "HEAD" : g_git_target.c_str());
        const bool can_submit = g_git_name[0] != '\0';
        ImGui::BeginDisabled(!can_submit);
        const bool submit = ImGui::Button("Create branch", {ui(125.0f), 0}) || submit_with_enter;
        if (submit && can_submit && g_git_manager->createBranch(repo(), g_git_name.data(),
                g_git_target, checkout_new_branch, g_notice)) {
            g_git_target.clear();
            g_git_target_label.clear();
            ImGui::CloseCurrentPopup();
        }
        ImGui::EndDisabled();
        ImGui::SameLine();
        if (ImGui::Button("Cancel", {ui(90.0f), 0})) {
            g_git_target.clear();
            g_git_target_label.clear();
            ImGui::CloseCurrentPopup();
        }
        ImGui::EndPopup();
    }

    if (g_tag_create_popup) {
        g_git_name.fill('\0');
        g_tag_create_popup = false;
        ImGui::OpenPopup("Create tag");
    }
    if (ImGui::BeginPopupModal("Create tag", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
        ImGui::TextUnformatted("Tag name");
        ImGui::SetNextItemWidth(ui(360.0f));
        ImGui::InputText("##tag_name", g_git_name.data(), g_git_name.size());
        ImGui::TextDisabled("Target: %s", g_git_target.empty() ? "HEAD" : g_git_target.c_str());
        ImGui::BeginDisabled(g_git_name[0] == '\0');
        if (ImGui::Button("Create", {ui(90.0f), 0}) &&
                g_git_manager->createTag(repo(), g_git_name.data(), g_git_target, g_notice)) {
            g_git_target.clear();
            ImGui::CloseCurrentPopup();
        }
        ImGui::EndDisabled();
        ImGui::SameLine();
        if (ImGui::Button("Cancel", {ui(90.0f), 0})) {
            g_git_target.clear();
            ImGui::CloseCurrentPopup();
        }
        ImGui::EndPopup();
    }

    if (g_remote_add_popup) {
        g_git_name.fill('\0');
        g_git_value.fill('\0');
        std::snprintf(g_git_name.data(), g_git_name.size(), "%s",
            std::find(repo().remotes.begin(), repo().remotes.end(), "origin") == repo().remotes.end()
                ? "origin" : "upstream");
        g_remote_add_popup = false;
        ImGui::OpenPopup("Add remote");
    }
    if (ImGui::BeginPopupModal("Add remote", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
        ImGui::TextUnformatted("Remote name");
        ImGui::SetNextItemWidth(ui(420.0f));
        ImGui::InputText("##remote_name", g_git_name.data(), g_git_name.size());
        ImGui::TextUnformatted("Remote URL");
        ImGui::SetNextItemWidth(ui(420.0f));
        ImGui::InputText("##remote_url", g_git_value.data(), g_git_value.size());
        ImGui::BeginDisabled(g_git_name[0] == '\0' || g_git_value[0] == '\0');
        if (ImGui::Button("Add remote", {ui(110.0f), 0}) &&
                g_git_manager->addRemote(repo(), g_git_name.data(), g_git_value.data(), g_notice))
            ImGui::CloseCurrentPopup();
        ImGui::EndDisabled();
        ImGui::SameLine();
        if (ImGui::Button("Cancel", {ui(90.0f), 0})) ImGui::CloseCurrentPopup();
        ImGui::EndPopup();
    }

    if (g_worktree_add_popup) {
        g_git_path.fill('\0');
        g_git_value.fill('\0');
        g_worktree_add_popup = false;
        ImGui::OpenPopup("Add worktree");
    }
    if (ImGui::BeginPopupModal("Add worktree", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
        ImGui::TextUnformatted("Worktree folder");
        ImGui::SetNextItemWidth(ui(460.0f));
        ImGui::InputText("##worktree_path", g_git_path.data(), g_git_path.size());
        ImGui::TextUnformatted("Branch or commit (optional)");
        ImGui::SetNextItemWidth(ui(460.0f));
        ImGui::InputText("##worktree_branch", g_git_value.data(), g_git_value.size());
        ImGui::BeginDisabled(g_git_path[0] == '\0');
        if (ImGui::Button("Add worktree", {ui(120.0f), 0}) &&
                g_git_manager->addWorktree(repo(), g_git_path.data(), g_git_value.data(), g_notice))
            ImGui::CloseCurrentPopup();
        ImGui::EndDisabled();
        ImGui::SameLine();
        if (ImGui::Button("Cancel", {ui(90.0f), 0})) ImGui::CloseCurrentPopup();
        ImGui::EndPopup();
    }

    if (g_submodule_add_popup) {
        g_git_value.fill('\0');
        g_git_path.fill('\0');
        g_submodule_add_popup = false;
        ImGui::OpenPopup("Add submodule");
    }
    if (ImGui::BeginPopupModal("Add submodule", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
        ImGui::TextUnformatted("Repository URL");
        ImGui::SetNextItemWidth(ui(460.0f));
        if (ImGui::IsWindowAppearing()) ImGui::SetKeyboardFocusHere();
        bool submit_with_enter = ImGui::InputText("##submodule_url",
            g_git_value.data(), g_git_value.size(), ImGuiInputTextFlags_EnterReturnsTrue);
        ImGui::TextUnformatted("Local path (optional)");
        ImGui::SetNextItemWidth(ui(460.0f));
        submit_with_enter |= ImGui::InputText("##submodule_path",
            g_git_path.data(), g_git_path.size(), ImGuiInputTextFlags_EnterReturnsTrue);
        const bool can_submit = g_git_value[0] != '\0';
        ImGui::BeginDisabled(!can_submit);
        const bool submit = ImGui::Button("Add submodule", {ui(125.0f), 0}) || submit_with_enter;
        if (submit && can_submit &&
                g_git_manager->addSubmodule(repo(), g_git_value.data(), g_git_path.data(), g_notice)) {
            g_git_value.fill('\0');
            g_git_path.fill('\0');
            ImGui::CloseCurrentPopup();
        }
        ImGui::EndDisabled();
        ImGui::SameLine();
        if (ImGui::Button("Cancel", {ui(90.0f), 0})) ImGui::CloseCurrentPopup();
        ImGui::EndPopup();
    }

    if (g_discard_all_popup) {
        g_discard_all_popup = false;
        ImGui::OpenPopup("Discard all changes?");
    }
    if (ImGui::BeginPopupModal("Discard all changes?", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
        ImGui::TextWrapped("This permanently discards tracked changes and removes untracked files and folders.");
        ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.42f, 0.12f, 0.13f, 1.0f));
        if (ImGui::Button("Discard all", {ui(110.0f), 0})) {
            if (g_git_manager->discardAll(repo(), g_notice)) ImGui::CloseCurrentPopup();
        }
        ImGui::PopStyleColor();
        ImGui::SameLine();
        if (ImGui::Button("Cancel", {ui(90.0f), 0})) ImGui::CloseCurrentPopup();
        ImGui::EndPopup();
    }
}

void draw_popups() {
    static int gitignore_template = 0;
    static int license_template = 0;
    static bool initialize_lfs = false;
    if (g_init_popup) {
        g_create_repository_name.fill('\0');
        set_path_buffer(g_create_repository_parent, default_repository_parent());
        std::snprintf(g_create_repository_branch.data(), g_create_repository_branch.size(), "main");
        gitignore_template = 0;
        license_template = 0;
        initialize_lfs = false;
        g_init_popup = false;
        ImGui::OpenPopup("Initialize a Repo");
    }
    ImGui::SetNextWindowSize({ui(720.0f), ui(405.0f)}, ImGuiCond_Appearing);
    if (ImGui::BeginPopupModal("Initialize a Repo", nullptr, ImGuiWindowFlags_NoResize)) {
        const float label_width = ui(145.0f);
        const float field_width = ui(555.0f);
        const auto field_label = [&](const char* label) {
            ImGui::AlignTextToFramePadding();
            ImGui::TextUnformatted(label);
            ImGui::SameLine(label_width);
        };
        field_label("Name");
        ImGui::SetNextItemWidth(field_width);
        if (ImGui::IsWindowAppearing()) ImGui::SetKeyboardFocusHere();
        ImGui::InputText("##create_repository_name", g_create_repository_name.data(),
            g_create_repository_name.size());
        field_label("Initialize in");
        ImGui::SetNextItemWidth(field_width - ui(82.0f));
        ImGui::InputText("##create_repository_parent", g_create_repository_parent.data(),
            g_create_repository_parent.size());
        ImGui::SameLine();
        if (ImGui::Button("Browse", {ui(72.0f), 0}))
            pick_folder_into(g_create_repository_parent, "Choose repository parent folder");

        const std::filesystem::path full_path = std::filesystem::path(g_create_repository_parent.data()) /
            g_create_repository_name.data();
        field_label("Full path");
        ImGui::TextDisabled("%s", full_path.string().c_str());
        field_label("Default branch name");
        ImGui::SetNextItemWidth(field_width);
        ImGui::InputTextWithHint("##create_repository_branch", "main",
            g_create_repository_branch.data(), g_create_repository_branch.size());

        constexpr const char* gitignore_options[] = {"None", "C / C++", "C#", "Python", "Node"};
        field_label(".gitignore template");
        ImGui::SetNextItemWidth(field_width);
        ImGui::Combo("##gitignore_template", &gitignore_template, gitignore_options,
            static_cast<int>(std::size(gitignore_options)));
        constexpr const char* license_options[] = {"None", "MIT", "Apache 2.0", "GPL-3.0"};
        field_label("License");
        ImGui::SetNextItemWidth(field_width);
        ImGui::Combo("##license_template", &license_template, license_options,
            static_cast<int>(std::size(license_options)));
        ImGui::SetCursorPosX(label_width);
        text_height_checkbox("Initialize with Git LFS", &initialize_lfs);

        ImGui::SetCursorPosY(ImGui::GetWindowHeight() - ui(48.0f));
        const float button_width = ui(125.0f);
        ImGui::SetCursorPosX(ImGui::GetWindowWidth() - button_width * 2.0f - ui(22.0f));
        if (ImGui::Button("Cancel", {button_width, ui(30.0f)})) ImGui::CloseCurrentPopup();
        ImGui::SameLine();
        const bool can_create = g_create_repository_name[0] && g_create_repository_parent[0] &&
            g_create_repository_branch[0];
        ImGui::BeginDisabled(!can_create);
        if (ImGui::Button("Create Repository", {button_width, ui(30.0f)}) && can_create) {
            if (repo().repo) create_new_tab();
            if (init_repository(full_path.string().c_str(), g_create_repository_branch.data())) {
                static constexpr const char* gitignores[] = {
                    "", "build/\n*.obj\n*.o\n*.exe\n.vs/\n.idea/\n",
                    "bin/\nobj/\n.vs/\n*.user\n", "__pycache__/\n*.py[cod]\n.venv/\n",
                    "node_modules/\ndist/\n.env\n",
                };
                static constexpr const char* licenses[] = {
                    "", "MIT License\n\nCopyright (c) 2026\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files to deal in the Software without restriction.\n",
                    "Apache License\nVersion 2.0, January 2004\nhttps://www.apache.org/licenses/LICENSE-2.0\n",
                    "GNU GENERAL PUBLIC LICENSE\nVersion 3, 29 June 2007\nhttps://www.gnu.org/licenses/gpl-3.0.txt\n",
                };
                if (gitignore_template > 0)
                    std::ofstream(full_path / ".gitignore") << gitignores[gitignore_template];
                if (license_template > 0)
                    std::ofstream(full_path / "LICENSE") << licenses[license_template];
                if (initialize_lfs) {
                    std::string output;
                    g_git_manager->captureGit(repo(), {"lfs", "install", "--local"}, output, g_notice);
                }
                if (g_git_manager->reload(repo(), g_notice)) mark_repository_refreshed(repo());
                ImGui::CloseCurrentPopup();
            }
        }
        ImGui::EndDisabled();
        ImGui::EndPopup();
    }

    static bool shallow_clone = false;
    static bool sparse_checkout = false;
    if (g_clone_popup) {
        set_path_buffer(g_clone_repository_parent, default_repository_parent());
        g_clone_repository_url.fill('\0');
        shallow_clone = false;
        sparse_checkout = false;
        g_clone_popup = false;
        ImGui::OpenPopup("Clone a Repo");
    }
    ImGui::SetNextWindowSize({ui(730.0f), ui(330.0f)}, ImGuiCond_Appearing);
    if (ImGui::BeginPopupModal("Clone a Repo", nullptr, ImGuiWindowFlags_NoResize)) {
        const float label_width = ui(165.0f);
        const float field_width = ui(540.0f);
        const auto field_label = [&](const char* label) {
            ImGui::AlignTextToFramePadding();
            ImGui::TextUnformatted(label);
            ImGui::SameLine(label_width);
        };
        field_label("Where to clone to");
        ImGui::SetNextItemWidth(field_width - ui(82.0f));
        ImGui::InputText("##clone_parent", g_clone_repository_parent.data(), g_clone_repository_parent.size());
        ImGui::SameLine();
        if (ImGui::Button("Browse", {ui(72.0f), 0}))
            pick_folder_into(g_clone_repository_parent, "Choose clone destination");
        field_label("URL");
        ImGui::SetNextItemWidth(field_width);
        if (ImGui::IsWindowAppearing()) ImGui::SetKeyboardFocusHere();
        ImGui::InputText("##clone_url", g_clone_repository_url.data(), g_clone_repository_url.size());
        ImGui::SetCursorPosX(label_width);
        text_height_checkbox("Shallow clone", &shallow_clone);
        ImGui::SetCursorPosX(label_width);
        text_height_checkbox("Sparse checkout", &sparse_checkout);

        ImGui::SetCursorPosY(ImGui::GetWindowHeight() - ui(48.0f));
        const float button_width = ui(120.0f);
        ImGui::SetCursorPosX(ImGui::GetWindowWidth() - button_width * 2.0f - ui(22.0f));
        if (ImGui::Button("Cancel", {button_width, ui(30.0f)})) ImGui::CloseCurrentPopup();
        ImGui::SameLine();
        const bool can_clone = g_clone_repository_parent[0] && g_clone_repository_url[0];
        ImGui::BeginDisabled(!can_clone);
        if (ImGui::Button("Clone Repository", {button_width, ui(30.0f)}) && can_clone) {
            std::string repository_name = g_clone_repository_url.data();
            while (!repository_name.empty() && (repository_name.back() == '/' || repository_name.back() == '\\'))
                repository_name.pop_back();
            const size_t separator = repository_name.find_last_of("/\\:");
            if (separator != std::string::npos) repository_name.erase(0, separator + 1);
            if (repository_name.ends_with(".git")) repository_name.resize(repository_name.size() - 4);
            const std::filesystem::path destination =
                std::filesystem::path(g_clone_repository_parent.data()) / repository_name;
            if (repo().repo) create_new_tab();
            if (g_git_manager->cloneRepository(repo(), g_clone_repository_url.data(),
                    destination.string(), shallow_clone, g_notice)) {
                if (g_user_data) g_user_data->addRecentRepository(destination.string());
                if (sparse_checkout) {
                    std::string output;
                    g_git_manager->captureGit(repo(), {"sparse-checkout", "init", "--cone"}, output, g_notice);
                }
                persist_repository_session();
                ImGui::CloseCurrentPopup();
            }
        }
        ImGui::EndDisabled();
        ImGui::EndPopup();
    }
    draw_about_popup();
    draw_licenses_popup();
    draw_git_action_popups();
}

void draw_footer() {
    ImGui::Separator();
    const float notice_alpha = footer_notice_alpha();
    const bool show_notice = !g_notice.empty() && notice_alpha > 0.02f;
    if (show_notice) {
        ImVec4 notice_color = ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled);
        notice_color.w *= notice_alpha;
        ImGui::PushStyleColor(ImGuiCol_TextDisabled, notice_color);
        ImGui::TextDisabled("%s", g_notice.c_str());
        ImGui::PopStyleColor();
    } else {
        ImGui::TextDisabled("Ready");
    }
    const char* version = "Gitree " GITREE_VERSION;
    const std::string zoom_label = std::string(ICON_TB_MAGNIFYING_GLASS "  ") +
        std::to_string(g_zoom_percent) + "%";
    const float zoom_width = ImGui::CalcTextSize(zoom_label.c_str()).x + ui(12.0f);
    const float version_width = ImGui::CalcTextSize(version).x;
    ImGui::SameLine(ImGui::GetWindowWidth() - zoom_width - version_width - ui(34.0f));
    ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
    ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.18f, 0.21f, 0.27f, 1.0f));
    ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {ui(6.0f), 0.0f});
    if (ImGui::Button(zoom_label.c_str(), {zoom_width, 0})) ImGui::OpenPopup("zoom_selector");
    ImGui::PopStyleVar();
    ImGui::PopStyleColor(2);
    if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort))
        ImGui::SetTooltip("UI zoom (Ctrl+mouse wheel is not required)");
    if (ImGui::BeginPopup("zoom_selector")) {
        constexpr int zoom_levels[] = {200, 175, 150, 140, 130, 120, 110, 100, 90, 80};
        for (const int zoom : zoom_levels) {
            const std::string label = std::to_string(zoom) + "%";
            if (ImGui::Selectable(label.c_str(), zoom == g_zoom_percent)) {
                g_zoom_percent = zoom;
                g_zoom_reload_requested = true;
                if (g_user_data) g_user_data->setZoomPercent(zoom);
                ImGui::CloseCurrentPopup();
            }
        }
        ImGui::EndPopup();
    }
    ImGui::SameLine(0, ui(12.0f));
    ImGui::TextDisabled("%s", version);
}

void draw_new_tab() {
    ImGui::BeginChild("new_tab", {-1, -ui(28.0f)}, ImGuiChildFlags_None);
    const float margin = ui(48.0f);
    const float content_width = std::min(ui(850.0f), ImGui::GetContentRegionAvail().x - margin * 2.0f);

    ImGui::SetCursorPos({margin, ui(28.0f)});
    ImGui::PushFont(g_bold_font, 0.0f);
    ImGui::SetWindowFontScale(1.25f);
    ImGui::TextUnformatted("Repositories");
    ImGui::SetWindowFontScale(1.0f);
    ImGui::PopFont();
    ImGui::SetCursorPosX(margin);
    if (ImGui::Button(ICON_TB_FOLDER_OPEN "  Open", {ui(112.0f), ui(40.0f)}))
        pick_and_open_repository();
    ImGui::SameLine(0, ui(8.0f));
    if (ImGui::Button(ICON_TB_DOWNLOAD "  Clone", {ui(116.0f), ui(40.0f)}))
        g_clone_popup = true;
    ImGui::SameLine(0, ui(8.0f));
    if (ImGui::Button(ICON_TB_PLUS "  Create", {ui(120.0f), ui(40.0f)}))
        g_init_popup = true;

    ImGui::SetCursorPosX(margin);
    ImGui::SetNextItemWidth(content_width);
    ImGui::InputTextWithHint("##repository_filter", ICON_TB_MAGNIFYING_GLASS "  Search repositories",
        g_repository_filter.data(), g_repository_filter.size());
    ImGui::Dummy({0, ui(10.0f)});
    ImGui::SetCursorPosX(margin);
    ImGui::PushFont(g_bold_font, 0.0f);
    ImGui::TextUnformatted("Recent");
    ImGui::PopFont();

    static std::filesystem::path discovered_root;
    static std::vector<std::string> discovered_repositories;
    const std::filesystem::path root = default_repository_parent();
    if (discovered_root != root) {
        discovered_root = root;
        discovered_repositories.clear();
        std::error_code error;
        for (std::filesystem::directory_iterator entry(root,
                std::filesystem::directory_options::skip_permission_denied, error), end;
                !error && entry != end; entry.increment(error)) {
            if (!entry->is_directory(error)) continue;
            if (std::filesystem::is_directory(entry->path() / ".git", error))
                discovered_repositories.push_back(entry->path().string());
            error.clear();
        }
        std::ranges::sort(discovered_repositories, {}, [](const std::string& path) {
            return std::filesystem::path(path).filename().string();
        });
    }

    std::vector<std::string> repositories;
    std::set<std::string> unique_paths;
    const auto add_repository = [&](const std::string& path) {
        if (!path.empty() && unique_paths.insert(path).second) repositories.push_back(path);
    };
    if (g_user_data)
        for (const std::string& path : g_user_data->recentRepositories()) add_repository(path);
    for (const std::string& path : discovered_repositories) add_repository(path);

    ImGui::SetCursorPosX(margin);
    ImGui::BeginChild("repository_list", {content_width, -ui(8.0f)}, ImGuiChildFlags_None);
    int visible_index = 0;
    for (const std::string& repository_path : repositories) {
        const std::filesystem::path path(repository_path);
        std::string name = path.filename().string();
        if (name.empty()) name = path.parent_path().filename().string();
        if (g_repository_filter[0] &&
                !contains_case_insensitive(name, g_repository_filter.data()) &&
                !contains_case_insensitive(repository_path, g_repository_filter.data()))
            continue;
        ImGui::PushID(visible_index++);
        const bool clicked = ImGui::Selectable("##repository", false,
            ImGuiSelectableFlags_None, {0, ui(28.0f)});
        const ImVec2 minimum = ImGui::GetItemRectMin();
        ImDrawList* draw = ImGui::GetWindowDrawList();
        const float row_font_size = ImGui::GetFontSize();
        const float text_y_offset = (ui(28.0f) - row_font_size) * 0.5f;
        draw->AddText(g_bold_font, row_font_size,
            {minimum.x + ui(8.0f), minimum.y + text_y_offset},
            IM_COL32(45, 192, 238, 255), name.c_str());
        const float name_width = g_bold_font->CalcTextSizeA(
            row_font_size, std::numeric_limits<float>::max(), 0.0f, name.c_str()).x;
        draw->AddText({minimum.x + ui(8.0f) + name_width + ui(14.0f), minimum.y + text_y_offset},
            IM_COL32(137, 143, 154, 255), repository_path.c_str());
        if (clicked) open_repository(repository_path.c_str());
        if (ImGui::BeginPopupContextItem()) {
            if (ImGui::MenuItem(ICON_TB_FOLDER_OPEN "  Open repository"))
                open_repository(repository_path.c_str());
            if (ImGui::MenuItem(ICON_TB_FOLDER_OPEN "  Reveal in file manager")) {
                std::string error;
                if (!izo::RevealInFileManager(repository_path, &error)) g_notice = error;
            }
            if (ImGui::MenuItem(ICON_TB_COPY "  Copy path"))
                copy_to_clipboard(repository_path, "path");
            ImGui::EndPopup();
        }
        ImGui::PopID();
    }
    if (visible_index == 0) ImGui::TextDisabled("No repositories found.");
    ImGui::EndChild();
    ImGui::EndChild();
}


void draw_app() {
    ImGuiViewport* viewport = ImGui::GetMainViewport();
    ImGui::SetNextWindowPos(viewport->WorkPos);
    ImGui::SetNextWindowSize(viewport->WorkSize);
    ImGui::Begin("Gitree", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
        ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar);

    const bool control_down = ImGui::GetIO().KeyCtrl && !ImGui::GetIO().KeyAlt;
    bool create_tab_requested = control_down && !ImGui::GetIO().KeyShift &&
        ImGui::IsKeyPressed(ImGuiKey_T, false);
    bool reopen_tab_requested = control_down && ImGui::GetIO().KeyShift &&
        ImGui::IsKeyPressed(ImGuiKey_T, false);
    bool close_tab_requested = control_down && ImGui::IsKeyPressed(ImGuiKey_W, false);
    bool open_repository_requested = control_down && !ImGui::GetIO().KeyShift &&
        ImGui::IsKeyPressed(ImGuiKey_O, false);
    bool create_repository_requested = control_down && !ImGui::GetIO().KeyShift &&
        ImGui::IsKeyPressed(ImGuiKey_N, false);
    const bool refresh_requested = ImGui::IsKeyPressed(ImGuiKey_F5, false);
    if (ImGui::BeginMenuBar()) {
        if (ImGui::BeginMenu("File")) {
            if (ImGui::MenuItem("New tab", "Ctrl+T")) create_tab_requested = true;
            if (ImGui::MenuItem("Close tab", "Ctrl+W")) close_tab_requested = true;
            ImGui::BeginDisabled(!g_user_data || g_user_data->recentlyClosed().empty());
            if (ImGui::MenuItem("Reopen closed tab", "Ctrl+Shift+T")) reopen_tab_requested = true;
            ImGui::EndDisabled();
            ImGui::Separator();
            if (ImGui::MenuItem("Open repository...", "Ctrl+O")) open_repository_requested = true;
            if (ImGui::MenuItem("Create repository...", "Ctrl+N")) create_repository_requested = true;
            ImGui::Separator();
            if (ImGui::MenuItem("Exit") && g_window_manager) g_window_manager->requestClose();
            ImGui::EndMenu();
        }
        if (ImGui::BeginMenu("Edit")) { ImGui::MenuItem("Preferences", nullptr, false, false); ImGui::EndMenu(); }
        if (ImGui::BeginMenu("View")) {
            if (ImGui::MenuItem("Refresh", "F5") || refresh_requested)
                reload_repository_background(repo(), true);
            ImGui::EndMenu();
        }
        if (ImGui::BeginMenu("Help")) {
            if (ImGui::MenuItem("About Gitree")) g_about_popup = true;
            ImGui::EndMenu();
        }
        ImGui::EndMenuBar();
    }

    if (close_tab_requested && !g_tabs.empty()) close_tab(g_active_tab);
    if (reopen_tab_requested && g_user_data) {
        const std::string path = g_user_data->takeRecentlyClosed();
        if (!path.empty()) {
            const bool created_tab = repo().repo != nullptr || !repo().path.empty();
            if (created_tab) create_new_tab();
            if (!open_repository(path.c_str())) {
                g_user_data->addRecentlyClosed(path);
                if (created_tab) close_tab(g_active_tab);
            }
        }
    }
    if (create_tab_requested) create_new_tab();
    if (open_repository_requested) pick_and_open_repository();
    if (create_repository_requested) g_init_popup = true;

    size_t tab_to_close = g_tabs.size();
    size_t tab_move_from = g_tabs.size();
    size_t tab_move_to = g_tabs.size();
    bool add_tab = false;
    const float current_tab_height = ImGui::GetFontSize() + ui(10.0f);
    const float taller_tab_padding = (current_tab_height * 1.10f - ImGui::GetFontSize()) * 0.5f;
    ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
        {ImGui::GetStyle().FramePadding.x, taller_tab_padding});
    ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {ImGui::GetStyle().ItemSpacing.x, 0.0f});
    const ImVec2 tab_strip_minimum = ImGui::GetCursorScreenPos();
    ImGui::GetWindowDrawList()->AddRectFilled(tab_strip_minimum,
        {tab_strip_minimum.x + ImGui::GetContentRegionAvail().x, tab_strip_minimum.y + ImGui::GetFrameHeight()},
        IM_COL32(43, 47, 56, 255));
    if (ImGui::BeginTabBar("repositories", ImGuiTabBarFlags_AutoSelectNewTabs)) {
        for (size_t i = 0; i < g_tabs.size(); ++i) {
            ImGui::PushID(g_tabs[i].get());
            bool open = true;
            const std::string label = (g_tabs[i]->repo ? std::string(ICON_TB_CODE_BRANCH "  ") : "") +
                g_tabs[i]->name + "###repo_tab";
            const ImGuiTabItemFlags flags = (g_tabs[i].get() == g_tab_to_select
                    ? ImGuiTabItemFlags_SetSelected : ImGuiTabItemFlags_None) |
                ImGuiTabItemFlags_NoReorder;
            ImGui::PushFont(g_bold_font, 0.0f);
            if (ImGui::BeginTabItem(label.c_str(), &open, flags)) {
                if (g_active_tab != i) activate_repository_tab(i);
                ImGui::EndTabItem();
            }
            ImGui::PopFont();
            if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) {
                ImGui::SetDragDropPayload("GITREE_REPOSITORY_TAB", &i, sizeof(i));
                ImGui::TextUnformatted(g_tabs[i]->name.c_str());
                ImGui::EndDragDropSource();
            }
            if (ImGui::BeginDragDropTarget()) {
                if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("GITREE_REPOSITORY_TAB")) {
                    tab_move_from = *static_cast<const size_t*>(payload->Data);
                    tab_move_to = i;
                }
                ImGui::EndDragDropTarget();
            }
            if (ImGui::BeginPopupContextItem()) {
                if (!g_tabs[i]->path.empty() && ImGui::MenuItem(ICON_TB_FOLDER_OPEN "  Reveal in file manager")) {
                    std::string error;
                    if (!izo::RevealInFileManager(g_tabs[i]->path, &error)) g_notice = error;
                }
                if (!g_tabs[i]->path.empty() && ImGui::MenuItem(ICON_TB_COPY "  Copy repository path"))
                    copy_to_clipboard(g_tabs[i]->path, "repository path");
                ImGui::Separator();
                if (ImGui::MenuItem(ICON_TB_XMARK "  Close tab")) tab_to_close = i;
                ImGui::EndPopup();
            }
            if (!open) tab_to_close = i;
            ImGui::PopID();
        }
        ImGui::SameLine(0, ui(6.0f));
        const float add_button_size = ImGui::GetFrameHeight();
        ImGui::InvisibleButton("##new_tab", {add_button_size, add_button_size});
        add_tab = ImGui::IsItemClicked();
        const ImVec2 add_minimum = ImGui::GetItemRectMin();
        const ImVec2 add_maximum = ImGui::GetItemRectMax();
        const bool add_hovered = ImGui::IsItemHovered();
        ImDrawList* tab_draw = ImGui::GetWindowDrawList();
        if (add_hovered)
            tab_draw->AddRectFilled(add_minimum, add_maximum, IM_COL32(51, 55, 63, 255), ui(5.0f));
        const ImVec2 add_icon_size = ImGui::CalcTextSize(ICON_TB_PLUS);
        tab_draw->AddText({
            add_minimum.x + (add_maximum.x - add_minimum.x - add_icon_size.x) * 0.5f,
            add_minimum.y + (add_maximum.y - add_minimum.y - add_icon_size.y) * 0.5f,
        }, IM_COL32(214, 221, 231, 255), ICON_TB_PLUS);
        if (add_hovered) ImGui::SetTooltip("New tab");
        ImGui::EndTabBar();
        g_tab_to_select = nullptr;
    }
    ImGui::PopStyleVar(2);
    if (tab_move_from < g_tabs.size() && tab_move_to < g_tabs.size() && tab_move_from != tab_move_to) {
        RepositoryView* active = g_tabs[g_active_tab].get();
        auto moved = std::move(g_tabs[tab_move_from]);
        g_tabs.erase(g_tabs.begin() + static_cast<std::ptrdiff_t>(tab_move_from));
        g_tabs.insert(g_tabs.begin() + static_cast<std::ptrdiff_t>(tab_move_to), std::move(moved));
        const auto active_tab = std::find_if(g_tabs.begin(), g_tabs.end(), [active](const auto& tab) {
            return tab.get() == active;
        });
        g_active_tab = static_cast<size_t>(std::distance(g_tabs.begin(), active_tab));
        persist_repository_session();
    }
    if (tab_to_close < g_tabs.size()) close_tab(tab_to_close);
    if (add_tab) create_new_tab();

    if (!repo().repo) {
        draw_new_tab();
        draw_footer();
        draw_popups();
        g_reset_repository_view = false;
        ImGui::End();
        return;
    }

    std::string branch_checkout_to_execute;
    if (!g_requested_branch_checkout.empty() && g_running_branch_checkout.empty()) {
        g_running_branch_checkout = std::move(g_requested_branch_checkout);
        g_requested_branch_checkout.clear();
        g_notice = std::string("Switching to branch ") + g_running_branch_checkout + "...";
    }
    if (!g_running_branch_checkout.empty()) branch_checkout_to_execute = g_running_branch_checkout;

    const ToolbarActionRequest toolbar_action_to_execute = g_pending_toolbar_action;
    if (toolbar_action_to_execute != ToolbarActionRequest::none) {
        g_running_toolbar_action = toolbar_action_to_execute;
        g_pending_toolbar_action = ToolbarActionRequest::none;
    }

    const bool code_viewer_open = g_diff_viewer.isOpen();
    if (code_viewer_open && ImGui::IsKeyPressed(ImGuiKey_Escape, false)) {
        g_diff_viewer.close();
    }
    if (code_viewer_open && !g_previous_code_viewer_open) {
        g_sidebar_collapsed_before_viewer = g_sidebar_collapsed;
        if (!g_sidebar_collapsed) {
            g_sidebar_collapsed = true;
            g_sidebar_auto_collapsed = true;
        } else {
            g_sidebar_auto_collapsed = false;
        }
    } else if (!code_viewer_open && g_previous_code_viewer_open) {
        if (g_sidebar_auto_collapsed) g_sidebar_collapsed = g_sidebar_collapsed_before_viewer;
        g_sidebar_auto_collapsed = false;
    }
    g_previous_code_viewer_open = code_viewer_open;

    ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {ImGui::GetStyle().ItemSpacing.x, 0.0f});
    ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(46 / 255.0f, 50 / 255.0f, 59 / 255.0f, 1.0f));
    ImGui::BeginChild("repo_toolbar", {-1, ui(50)}, ImGuiChildFlags_None,
        ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
    ImGui::SetCursorPos({ui(5), ui(2)});
    ImGui::PushFont(g_bold_font, 0.0f);
    const float repository_width = std::clamp(
        ImGui::CalcTextSize(repo().name.c_str()).x / g_ui_scale + 42.0f, 120.0f, 220.0f);
    const bool repository_selector_clicked =
        toolbar_selector("repository", "repository", repo().name, repository_width);
    ImGui::PopFont();
    if (repository_selector_clicked)
        ImGui::OpenPopup("repository_selector");
    if (ImGui::BeginPopup("repository_selector")) {
        for (size_t index = 0; index < g_tabs.size(); ++index) {
            if (ImGui::MenuItem(g_tabs[index]->name.c_str(), nullptr, index == g_active_tab)) {
                activate_repository_tab(index);
            }
        }
        ImGui::EndPopup();
    }
    ImGui::SameLine(0, ui(3));
    ImGui::SetCursorPosY(ui(23));
    ImGui::TextDisabled(ICON_TB_ANGLE_RIGHT);
    ImGui::SameLine(0, ui(3));
    ImGui::SetCursorPosY(ui(2));
    if (g_request_branch_selector) {
        ImGui::OpenPopup("branch_selector");
        g_request_branch_selector = false;
    }
    const bool switching_branch = !branch_checkout_to_execute.empty();
    const std::string branch_selector_value = switching_branch
        ? std::string(ICON_TB_ROTATE_RIGHT) + " Switching..."
        : repo().branch;
    if (!switching_branch && toolbar_selector("branch", "branch", branch_selector_value, 150.0f))
        ImGui::OpenPopup("branch_selector");
    const float selectors_right = ImGui::GetItemRectMax().x - ImGui::GetWindowPos().x;
    const ImVec2 branch_popup_size{ui(320.0f), ui(370.0f)};
    ImGui::SetNextWindowSize(branch_popup_size, ImGuiCond_Always);
    ImGui::SetNextWindowSizeConstraints(branch_popup_size, branch_popup_size);
    if (!switching_branch && ImGui::BeginPopup("branch_selector", ImGuiWindowFlags_NoResize)) {
        if (ImGui::IsWindowAppearing()) ImGui::SetKeyboardFocusHere();
        ImGui::SetNextItemWidth(-1);
        ImGui::InputTextWithHint("##branch_search", ICON_TB_MAGNIFYING_GLASS "  Search branches...",
            g_branch_filter.data(), g_branch_filter.size());
        ImGui::Separator();
        ImGui::TextDisabled("LOCAL BRANCHES");
        ImGui::BeginChild("branch_results", {-1, -1}, ImGuiChildFlags_None);
        for (const auto& branch : repo().local_branches) {
            if (!contains_case_insensitive(branch, g_branch_filter.data())) continue;
            const std::string item = (branch == repo().branch ? std::string(ICON_TB_CHECK "  ") : "   ") + branch;
            if (ImGui::Selectable(item.c_str(), branch == repo().branch) && branch != repo().branch) {
                request_branch_checkout(branch);
                clear_sidebar_filter();
                ImGui::CloseCurrentPopup();
            }
        }
        if (repo().local_branches.empty()) ImGui::TextDisabled("No local branches");
        ImGui::EndChild();
        ImGui::EndPopup();
    }

    const float action_spacing = ui(3.0f);
    const float action_group_width = ui(418.0f);
    const float centered_x = (ImGui::GetWindowWidth() - action_group_width) * 0.5f;
    const bool toolbar_busy = g_running_toolbar_action != ToolbarActionRequest::none;
    ImGui::SetCursorPos({std::max(selectors_right + ui(10.0f), centered_x), ui(2.0f)});

    if (toolbar_action("undo", "Undo", ICON_TB_ROTATE_LEFT, repo().undo_action.tooltip.c_str(),
            repo().undo_action.available, false, 54))
        g_git_manager->undoCommit(repo(), g_notice);
    ImGui::SameLine(0, action_spacing);
    if (toolbar_action("redo", "Redo", ICON_TB_ROTATE_RIGHT, repo().redo_action.tooltip.c_str(),
            repo().redo_action.available, false, 54))
        g_git_manager->redoCommit(repo(), g_notice);
    ImGui::SameLine(0, action_spacing);
    bool pull_dropdown_clicked = false;
    const bool pull_running = g_running_toolbar_action == ToolbarActionRequest::pull;
    if (toolbar_action("pull", "Pull", pull_running ? ICON_TB_ROTATE_RIGHT : ICON_TB_DOWNLOAD,
            pull_running ? "Pull in progress..." : pull_mode_name(g_user_data->pullMode()),
            !toolbar_busy, true, 58, pull_running, &pull_dropdown_clicked))
        g_pending_toolbar_action = ToolbarActionRequest::pull;
    if (pull_dropdown_clicked) ImGui::OpenPopup("pull_options");
    draw_pull_options();
    ImGui::SameLine(0, action_spacing);
    const bool push_running = g_running_toolbar_action == ToolbarActionRequest::push;
    if (toolbar_action("push", "Push", push_running ? ICON_TB_ROTATE_RIGHT : ICON_TB_UPLOAD,
            push_running ? "Push in progress..." : "Push to remote", !toolbar_busy, false, 58, push_running))
        g_pending_toolbar_action = ToolbarActionRequest::push;
    ImGui::SameLine(0, action_spacing);
    if (toolbar_action("branch_action", "Branch", ICON_TB_CODE_BRANCH, "Create branch", true, false, 64)) {
        begin_inline_branch(repo().selected_commit >= 0 ? repo().selected_commit : 0);
    }
    ImGui::SameLine(0, action_spacing);
    if (toolbar_action("stash", "Stash", ICON_TB_BOX_ARCHIVE, "Stash changes", true, false, 58)) {
        start_stash_async(repo(), false);
    }
    ImGui::SameLine(0, action_spacing);
    if (toolbar_action("pop", "Pop", ICON_TB_BOX_OPEN, "Pop latest stash", true, false, 54)) {
        start_stash_async(repo(), true);
    }
    draw_open_in_button();
    ImGui::EndChild();
    ImGui::PopStyleColor();
    ImGui::PopStyleVar();

    draw_sidebar(ui(g_sidebar_collapsed ? 44.0f : g_sidebar_width));
    ImGui::SameLine(0, 0);
    if (!g_sidebar_collapsed) {
        ImGui::InvisibleButton("##sidebar_splitter", {ui(6.0f), -ui(28.0f)});
        if (ImGui::IsItemHovered() || ImGui::IsItemActive()) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW);
        if (ImGui::IsItemActive()) {
            g_sidebar_width = std::clamp(
                g_sidebar_width + ImGui::GetIO().MouseDelta.x / g_ui_scale, 180.0f, 520.0f);
        }
        const ImVec2 splitter_min = ImGui::GetItemRectMin();
        const ImVec2 splitter_max = ImGui::GetItemRectMax();
        ImGui::GetWindowDrawList()->AddLine(
            {(splitter_min.x + splitter_max.x) * 0.5f, splitter_min.y},
            {(splitter_min.x + splitter_max.x) * 0.5f, splitter_max.y},
            ImGui::IsItemHovered() || ImGui::IsItemActive()
                ? IM_COL32(23, 181, 204, 255) : IM_COL32(0, 0, 0, 0), ui(1.0f));
        ImGui::SameLine(0, 0);
    }
    const float content_width = ImGui::GetContentRegionAvail().x;
    const float details_maximum = std::max(180.0f,
        std::min(650.0f, (content_width - ui(306.0f)) / g_ui_scale));
    g_details_width = std::clamp(g_details_width, std::min(280.0f, details_maximum), details_maximum);
    const float detail_width = ui(g_details_width);
    if (g_reset_repository_view) ImGui::SetNextWindowScroll({0.0f, 0.0f});
    ImGui::BeginChild("center", {content_width - detail_width - ui(6.0f), -ui(28.0f)}, false);
    if (g_diff_viewer.isOpen())
        g_diff_viewer.draw(repo(), *g_git_manager, g_avatar_cache, g_ui_scale, g_code_font, g_notice);
    else draw_commit_table();
    ImGui::EndChild();
    ImGui::SameLine(0, 0);
    ImGui::InvisibleButton("##details_splitter", {ui(6.0f), -ui(28.0f)});
    const bool details_splitter_active = ImGui::IsItemActive();
    const bool details_splitter_hovered = ImGui::IsItemHovered();
    if (details_splitter_active || details_splitter_hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW);
    if (details_splitter_active) {
        g_details_width = std::clamp(g_details_width - ImGui::GetIO().MouseDelta.x / g_ui_scale,
            std::min(280.0f, details_maximum), details_maximum);
        g_user_data->setDetailsWidth(g_details_width);
    }
    if (ImGui::IsItemDeactivated()) g_user_data->save();
    const ImVec2 details_splitter_min = ImGui::GetItemRectMin();
    const ImVec2 details_splitter_max = ImGui::GetItemRectMax();
    ImGui::GetWindowDrawList()->AddLine(
        {(details_splitter_min.x + details_splitter_max.x) * 0.5f, details_splitter_min.y},
        {(details_splitter_min.x + details_splitter_max.x) * 0.5f, details_splitter_max.y},
        details_splitter_active || details_splitter_hovered
            ? IM_COL32(23, 181, 204, 255) : IM_COL32(0, 0, 0, 0), ui(1.0f));
    ImGui::SameLine(0, 0);
    draw_details(detail_width);
    g_reset_repository_view = false;

    draw_footer();
    draw_popups();
    ImGui::End();

    if (toolbar_action_to_execute != ToolbarActionRequest::none &&
            start_toolbar_action_async(repo(), toolbar_action_to_execute))
        g_running_toolbar_action = toolbar_action_to_execute;

    if (!branch_checkout_to_execute.empty()) {
        if (!g_running_branch_checkout.empty() && start_branch_checkout_async(repo(), branch_checkout_to_execute))
            g_running_branch_checkout = branch_checkout_to_execute;
    }
}

} // namespace

int runGitree(int argc, char** argv) {
    GitManager git_manager;
    g_git_manager = &git_manager;
    ApplicationManager application_manager;
    g_application_manager = &application_manager;
    g_open_in_application = application_manager.defaultApplication();
    UserData user_data;
    g_user_data = &user_data;
    g_sidebar_width = user_data.sidebarWidth();
    g_details_width = user_data.detailsWidth();
    g_sidebar_collapsed = user_data.sidebarCollapsed();
    g_zoom_percent = user_data.zoomPercent();

    if (argc > 1) {
        create_new_tab(false);
        open_repository(argv[1]);
    } else if (!user_data.openRepositories().empty()) {
        for (const std::string& path : user_data.openRepositories()) {
            g_tabs.push_back(std::make_unique<RepositoryView>());
            if (!path.empty() && git_manager.openRepositoryHandle(*g_tabs.back(), path, g_notice))
                g_tabs.back()->pending_background_refresh = true;
        }
        g_active_tab = std::min(user_data.activeRepository(), g_tabs.size() - 1);
        g_tab_to_select = g_tabs[g_active_tab].get();
    } else {
        create_new_tab(false);
    }

    WindowManager window_manager;
    if (!window_manager.create("Gitree", 1500, 900)) return 1;
    g_window_manager = &window_manager;
    AvatarCache avatar_cache(user_data.directory());
    g_avatar_cache = &avatar_cache;
    ApplicationIconCache application_icon_cache;
    g_application_icon_cache = &application_icon_cache;

    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
    ImGui::GetIO().IniFilename = nullptr;
    load_fonts(combined_ui_scale(window_manager.dpiScale(), g_zoom_percent));
    ImGui_ImplGlfw_InitForOpenGL(window_manager.nativeWindow(), true);
    ImGui_ImplOpenGL3_Init("#version 330");

    while (!window_manager.shouldClose()) {
        window_manager.pollEvents();
        process_repository_background_refreshes();
        const bool dpi_changed = window_manager.consumeDpiChange();
        if (dpi_changed || g_zoom_reload_requested) {
            load_fonts(combined_ui_scale(window_manager.dpiScale(), g_zoom_percent));
            g_zoom_reload_requested = false;
        }
        ImGui_ImplOpenGL3_NewFrame();
        ImGui_ImplGlfw_NewFrame();
        ImGui::NewFrame();
        avatar_cache.update();
        draw_app();
        draw_toasts();
        ImGui::Render();
        int width = 0, height = 0;
        glfwGetFramebufferSize(window_manager.nativeWindow(), &width, &height);
        glViewport(0, 0, width, height);
        glClearColor(28.0f / 255.0f, 30.0f / 255.0f, 35.0f / 255.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);
        ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
        window_manager.swapBuffers();
    }

    avatar_cache.shutdown();
    application_icon_cache.shutdown();
    g_avatar_cache = nullptr;
    g_application_icon_cache = nullptr;
    g_application_manager = nullptr;
    ImGui_ImplOpenGL3_Shutdown();
    ImGui_ImplGlfw_Shutdown();
    user_data.setSidebarCollapsed(g_sidebar_auto_collapsed ? g_sidebar_collapsed_before_viewer : g_sidebar_collapsed);
    user_data.setSidebarWidth(g_sidebar_width);
    user_data.setDetailsWidth(g_details_width);
    persist_repository_session();
    user_data.save();
    ImGui::DestroyContext();
    g_window_manager = nullptr;
    g_tabs.clear();
    g_user_data = nullptr;
    g_git_manager = nullptr;
    return 0;
}
