refactor(core): separate managers models and UI

This commit is contained in:
2026-06-18 16:53:08 -05:00
parent 2dcb2036cb
commit 77b2fbe468
14 changed files with 1068 additions and 1022 deletions

View File

@@ -50,14 +50,19 @@ find_package(OpenGL REQUIRED)
add_executable(gitree WIN32
src/main.cpp
src/window_manager.cpp
src/window_manager.h
src/user_data.cpp
src/user_data.h
src/avatar_cache.cpp
src/avatar_cache.h
src/ui/gitree_ui.cpp
src/ui/gitree_ui.h
src/managers/git_manager.cpp
src/managers/git_manager.h
src/managers/window_manager.cpp
src/managers/window_manager.h
src/managers/user_data.cpp
src/managers/user_data.h
src/managers/avatar_cache.cpp
src/managers/avatar_cache.h
src/models/repository.h
)
target_include_directories(gitree PRIVATE vendor/libgit2/include vendor/icons)
target_include_directories(gitree PRIVATE src vendor/libgit2/include vendor/icons)
target_link_libraries(gitree PRIVATE imgui libgit2package iZo::izo OpenGL::GL)
target_compile_definitions(gitree PRIVATE
GITREE_VERSION="${PROJECT_VERSION}"

View File

@@ -33,3 +33,10 @@ User settings, ImGui layout state, and recently closed repository history are st
`%APPDATA%\Identity\Gitree` on Windows. The directory is created automatically.
Commit avatars are resolved from normalized author emails through Gravatar and cached in
the `avatars` subdirectory; an identicon is used when an account has no custom image.
## Source layout
- `src/managers/` owns Git, window, user-data, and avatar services.
- `src/models/` contains repository and commit data models.
- `src/ui/` contains the Dear ImGui application interface.
- `src/main.cpp` is the minimal process entry point.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,214 @@
#include "managers/git_manager.h"
#include <algorithm>
#include <ctime>
#include <filesystem>
namespace {
void addBadge(RepositoryView& repository, const git_oid* oid, RefBadge badge) {
if (!oid) return;
const auto commit = std::find_if(repository.commits.begin(), repository.commits.end(),
[oid](const CommitInfo& item) { return git_oid_equal(&item.oid, oid) != 0; });
if (commit != repository.commits.end()) commit->refs.push_back(std::move(badge));
}
int submoduleCallback(git_submodule* submodule, const char*, void* payload) {
static_cast<std::vector<std::string>*>(payload)->emplace_back(git_submodule_name(submodule));
return 0;
}
}
GitManager::GitManager() { git_libgit2_init(); }
GitManager::~GitManager() { git_libgit2_shutdown(); }
std::string GitManager::lastError(const char* fallback) {
const git_error* error = git_error_last();
return error && error->message ? error->message : fallback;
}
std::string GitManager::formatTime(git_time_t value, int offset_minutes) {
std::time_t adjusted = static_cast<std::time_t>(value + offset_minutes * 60);
std::tm tm{};
#ifdef _WIN32
gmtime_s(&tm, &adjusted);
#else
gmtime_r(&adjusted, &tm);
#endif
char buffer[48]{};
std::strftime(buffer, sizeof(buffer), "%m/%d/%Y %I:%M %p", &tm);
return buffer;
}
void GitManager::readBranches(RepositoryView& repository, git_branch_t type,
std::vector<std::string>& output) {
git_branch_iterator* iterator = nullptr;
if (git_branch_iterator_new(&iterator, repository.repo, type) != 0) return;
git_reference* reference = nullptr;
git_branch_t found{};
while (git_branch_next(&reference, &found, iterator) == 0) {
const char* name = nullptr;
if (git_branch_name(&name, reference) == 0 && name) output.emplace_back(name);
git_reference_free(reference);
}
git_branch_iterator_free(iterator);
}
void GitManager::loadRefBadges(RepositoryView& repository) {
for (const git_branch_t type : {GIT_BRANCH_LOCAL, GIT_BRANCH_REMOTE}) {
git_branch_iterator* iterator = nullptr;
if (git_branch_iterator_new(&iterator, repository.repo, type) != 0) continue;
git_reference* reference = nullptr;
git_branch_t found{};
while (git_branch_next(&reference, &found, iterator) == 0) {
const char* name = nullptr;
if (git_branch_name(&name, reference) == 0 && name) {
RefBadge badge;
badge.name = name;
badge.kind = type == GIT_BRANCH_LOCAL ? RefKind::local : RefKind::remote;
badge.current = type == GIT_BRANCH_LOCAL && badge.name == repository.branch;
badge.worktree = type == GIT_BRANCH_LOCAL &&
(badge.current || repository.worktree_branches.contains(badge.name));
if (type == GIT_BRANCH_LOCAL) {
git_reference* upstream = nullptr;
badge.upstream = git_branch_upstream(&upstream, reference) == 0;
git_reference_free(upstream);
}
git_object* object = nullptr;
if (git_reference_peel(&object, reference, GIT_OBJECT_COMMIT) == 0) {
addBadge(repository, git_object_id(object), std::move(badge));
git_object_free(object);
}
}
git_reference_free(reference);
}
git_branch_iterator_free(iterator);
}
git_reference_iterator* tags = nullptr;
if (git_reference_iterator_glob_new(&tags, repository.repo, "refs/tags/*") != 0) return;
git_reference* reference = nullptr;
while (git_reference_next(&reference, tags) == 0) {
git_object* object = nullptr;
if (git_reference_peel(&object, reference, GIT_OBJECT_COMMIT) == 0) {
const char* name = git_reference_shorthand(reference);
addBadge(repository, git_object_id(object), {name ? name : "tag", RefKind::tag});
git_object_free(object);
}
git_reference_free(reference);
}
git_reference_iterator_free(tags);
}
void GitManager::computeGraphLanes(RepositoryView& repository) {
std::vector<git_oid> lanes;
for (auto& commit : repository.commits) {
auto current = std::find_if(lanes.begin(), lanes.end(), [&commit](const git_oid& oid) {
return git_oid_equal(&oid, &commit.oid) != 0;
});
if (current == lanes.end()) { lanes.push_back(commit.oid); current = std::prev(lanes.end()); }
const size_t lane = static_cast<size_t>(std::distance(lanes.begin(), current));
commit.lane = static_cast<int>(lane);
if (commit.parent_ids.empty()) { lanes.erase(lanes.begin() + static_cast<std::ptrdiff_t>(lane)); continue; }
const git_oid first = commit.parent_ids.front();
const auto existing = std::find_if(lanes.begin(), lanes.end(), [&first](const git_oid& oid) {
return git_oid_equal(&oid, &first) != 0;
});
if (existing != lanes.end() && existing != current) lanes.erase(lanes.begin() + static_cast<std::ptrdiff_t>(lane));
else lanes[lane] = first;
for (size_t parent = 1; parent < commit.parent_ids.size(); ++parent) {
const auto found = std::find_if(lanes.begin(), lanes.end(), [&commit, parent](const git_oid& oid) {
return git_oid_equal(&oid, &commit.parent_ids[parent]) != 0;
});
if (found == lanes.end()) lanes.push_back(commit.parent_ids[parent]);
}
}
}
bool GitManager::loadRepositoryData(RepositoryView& repository, std::string& error) {
repository.local_branches.clear(); repository.remote_branches.clear(); repository.remotes.clear();
repository.worktrees.clear(); repository.worktree_branches.clear(); repository.submodules.clear();
repository.commits.clear(); repository.selected_commit = 0;
const char* workdir = git_repository_workdir(repository.repo);
repository.path = workdir ? workdir : git_repository_path(repository.repo);
std::filesystem::path path(repository.path);
repository.name = path.filename().string();
if (repository.name.empty()) repository.name = path.parent_path().filename().string();
git_reference* head = nullptr;
if (git_repository_head(&head, repository.repo) == 0) {
if (const char* name = git_reference_shorthand(head)) repository.branch = name;
git_reference_free(head);
}
readBranches(repository, GIT_BRANCH_LOCAL, repository.local_branches);
readBranches(repository, GIT_BRANCH_REMOTE, repository.remote_branches);
git_strarray names{};
if (git_remote_list(&names, repository.repo) == 0) {
for (size_t i = 0; i < names.count; ++i) repository.remotes.emplace_back(names.strings[i]);
git_strarray_dispose(&names);
}
if (git_worktree_list(&names, repository.repo) == 0) {
for (size_t i = 0; i < names.count; ++i) {
repository.worktrees.emplace_back(names.strings[i]);
git_worktree* worktree = nullptr;
git_repository* worktree_repository = nullptr;
git_reference* worktree_head = nullptr;
if (git_worktree_lookup(&worktree, repository.repo, names.strings[i]) == 0 &&
git_repository_open_from_worktree(&worktree_repository, worktree) == 0 &&
git_repository_head(&worktree_head, worktree_repository) == 0) {
if (const char* branch = git_reference_shorthand(worktree_head))
repository.worktree_branches.emplace(branch);
}
git_reference_free(worktree_head);
git_repository_free(worktree_repository);
git_worktree_free(worktree);
}
git_strarray_dispose(&names);
}
git_submodule_foreach(repository.repo, submoduleCallback, &repository.submodules);
git_revwalk* walk = nullptr;
if (git_revwalk_new(&walk, repository.repo) != 0) { error = lastError("Unable to read history"); return false; }
git_revwalk_sorting(walk, GIT_SORT_TOPOLOGICAL | GIT_SORT_TIME);
if (git_revwalk_push_head(walk) != 0) { git_revwalk_free(walk); return true; }
git_oid oid{};
while (repository.commits.size() < 250 && git_revwalk_next(&oid, walk) == 0) {
git_commit* commit = nullptr;
if (git_commit_lookup(&commit, repository.repo, &oid) != 0) continue;
CommitInfo item; item.oid = oid;
char id[GIT_OID_SHA1_HEXSIZE + 1]{}; git_oid_tostr(id, sizeof(id), &oid); item.short_id.assign(id, 8);
item.summary = git_commit_summary(commit) ? git_commit_summary(commit) : "(no commit message)";
if (const git_signature* author = git_commit_author(commit)) {
item.author = author->name ? author->name : "Unknown";
item.email = author->email ? author->email : "";
item.date = formatTime(author->when.time, author->when.offset);
}
item.parents = static_cast<int>(git_commit_parentcount(commit));
for (size_t i = 0; i < git_commit_parentcount(commit); ++i)
if (const git_oid* parent = git_commit_parent_id(commit, static_cast<unsigned int>(i))) item.parent_ids.push_back(*parent);
repository.commits.push_back(std::move(item));
git_commit_free(commit);
}
git_revwalk_free(walk);
computeGraphLanes(repository);
loadRefBadges(repository);
return true;
}
bool GitManager::openRepository(RepositoryView& repository, const std::string& path, std::string& error) {
git_repository* opened = nullptr;
if (git_repository_open_ext(&opened, path.c_str(), 0, nullptr) != 0) { error = lastError("Unable to open repository"); return false; }
repository.close(); repository.repo = opened;
return loadRepositoryData(repository, error);
}
bool GitManager::initializeRepository(RepositoryView& repository, const std::string& path, std::string& error) {
git_repository* created = nullptr;
if (git_repository_init(&created, path.c_str(), 0) != 0) { error = lastError("Unable to initialize repository"); return false; }
git_repository_free(created);
return openRepository(repository, path, error);
}
bool GitManager::reload(RepositoryView& repository, std::string& error) {
return repository.repo && loadRepositoryData(repository, error);
}

View File

@@ -0,0 +1,22 @@
#pragma once
#include "models/repository.h"
#include <string>
class GitManager {
public:
GitManager();
~GitManager();
bool openRepository(RepositoryView& repository, const std::string& path, std::string& error);
bool initializeRepository(RepositoryView& repository, const std::string& path, std::string& error);
bool reload(RepositoryView& repository, std::string& error);
private:
static std::string lastError(const char* fallback);
static std::string formatTime(git_time_t value, int offset_minutes);
void readBranches(RepositoryView& repository, git_branch_t type, std::vector<std::string>& output);
void loadRefBadges(RepositoryView& repository);
void computeGraphLanes(RepositoryView& repository);
bool loadRepositoryData(RepositoryView& repository, std::string& error);
};

View File

@@ -2,6 +2,7 @@
#include <GLFW/glfw3.h>
#include <algorithm>
#include <cstring>
#ifdef _WIN32
#define GLFW_EXPOSE_NATIVE_WIN32
@@ -18,8 +19,10 @@ bool WindowManager::create(const char* title, int width, int height) {
#ifdef _WIN32
using SetDpiAwarenessContext = BOOL(WINAPI*)(HANDLE);
const HMODULE user32 = GetModuleHandleW(L"user32.dll");
const auto set_dpi_awareness = reinterpret_cast<SetDpiAwarenessContext>(
GetProcAddress(user32, "SetProcessDpiAwarenessContext"));
const FARPROC dpi_address = GetProcAddress(user32, "SetProcessDpiAwarenessContext");
SetDpiAwarenessContext set_dpi_awareness = nullptr;
static_assert(sizeof(set_dpi_awareness) == sizeof(dpi_address));
std::memcpy(&set_dpi_awareness, &dpi_address, sizeof(set_dpi_awareness));
if (set_dpi_awareness) set_dpi_awareness(reinterpret_cast<HANDLE>(-4)); // Per-monitor v2
#endif
if (!glfwInit()) return false;

54
src/models/repository.h Normal file
View File

@@ -0,0 +1,54 @@
#pragma once
#include <git2.h>
#include <set>
#include <string>
#include <vector>
enum class RefKind { local, remote, tag };
struct RefBadge {
std::string name;
RefKind kind = RefKind::local;
bool current = false;
bool worktree = false;
bool upstream = false;
};
struct CommitInfo {
git_oid oid{};
std::string short_id;
std::string summary;
std::string author;
std::string email;
std::string date;
int parents = 0;
int lane = 0;
std::vector<git_oid> parent_ids;
std::vector<RefBadge> refs;
};
struct RepositoryView {
git_repository* repo = nullptr;
std::string path;
std::string name = "New Tab";
std::string branch = "detached";
std::vector<std::string> local_branches;
std::vector<std::string> remote_branches;
std::vector<std::string> remotes;
std::vector<std::string> worktrees;
std::set<std::string> worktree_branches;
std::vector<std::string> submodules;
std::vector<CommitInfo> commits;
int selected_commit = 0;
RepositoryView() = default;
~RepositoryView() { close(); }
RepositoryView(const RepositoryView&) = delete;
RepositoryView& operator=(const RepositoryView&) = delete;
void close() {
if (repo) git_repository_free(repo);
repo = nullptr;
}
};

749
src/ui/gitree_ui.cpp Normal file
View File

@@ -0,0 +1,749 @@
#include <git2.h>
#include <GLFW/glfw3.h>
#include <imgui.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_opengl3.h>
#include <izo/dialogs.hpp>
#include <IconsFontAwesome6.h>
#include "ui/gitree_ui.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 <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <filesystem>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <vector>
namespace {
std::vector<std::unique_ptr<RepositoryView>> g_tabs;
size_t g_active_tab = 0;
UserData* g_user_data = nullptr;
AvatarCache* g_avatar_cache = nullptr;
std::array<char, 1024> g_path{};
std::array<char, 256> g_filter{};
std::string g_notice;
bool g_init_popup = false;
bool g_about_popup = false;
bool g_licenses_popup = false;
float g_ui_scale = 1.0f;
float g_sidebar_width = 230.0f;
WindowManager* g_window_manager = nullptr;
GitManager* g_git_manager = nullptr;
float ui(float value) { return value * g_ui_scale; }
RepositoryView& repo() { return *g_tabs.at(g_active_tab); }
void create_new_tab() {
g_tabs.push_back(std::make_unique<RepositoryView>());
g_active_tab = g_tabs.size() - 1;
}
void close_tab(size_t index) {
if (index >= g_tabs.size()) return;
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;
}
bool open_repository(const char* path) {
return g_git_manager->openRepository(repo(), path, g_notice);
}
void pick_and_open_repository() {
izo::dialog_options options;
options.title = "Open Git repository";
if (!repo().path.empty()) options.initial_path = repo().path;
else options.initial_path = std::filesystem::current_path();
const izo::dialog_result result = izo::pick_folder(options);
if (result.status == izo::dialog_status::cancelled) return;
if (result.status == izo::dialog_status::error) {
g_notice = result.error_message.empty() ? "Unable to open the folder picker" : result.error_message;
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) {
return g_git_manager->initializeRepository(repo(), path, g_notice);
}
void apply_style(float scale) {
g_ui_scale = scale;
ImGuiStyle& style = ImGui::GetStyle();
style = ImGuiStyle();
style.WindowRounding = 0.0f;
style.ChildRounding = 3.0f;
style.FrameRounding = 3.0f;
style.PopupRounding = 4.0f;
style.ScrollbarRounding = 3.0f;
style.FramePadding = {8.0f, 5.0f};
style.ItemSpacing = {8.0f, 6.0f};
style.WindowPadding = {8.0f, 8.0f};
style.Colors[ImGuiCol_Text] = ImVec4(0.89f, 0.91f, 0.94f, 1.0f);
style.Colors[ImGuiCol_TextDisabled] = ImVec4(0.53f, 0.58f, 0.66f, 1.0f);
style.Colors[ImGuiCol_WindowBg] = ImVec4(0.100f, 0.116f, 0.145f, 1.0f);
style.Colors[ImGuiCol_ChildBg] = ImVec4(0.116f, 0.133f, 0.165f, 1.0f);
style.Colors[ImGuiCol_PopupBg] = ImVec4(0.13f, 0.15f, 0.19f, 1.0f);
style.Colors[ImGuiCol_Border] = ImVec4(0.24f, 0.28f, 0.34f, 1.0f);
style.Colors[ImGuiCol_FrameBg] = ImVec4(0.16f, 0.19f, 0.23f, 1.0f);
style.Colors[ImGuiCol_FrameBgHovered] = ImVec4(0.20f, 0.24f, 0.29f, 1.0f);
style.Colors[ImGuiCol_Button] = ImVec4(0.16f, 0.19f, 0.23f, 1.0f);
style.Colors[ImGuiCol_ButtonHovered] = ImVec4(0.10f, 0.44f, 0.56f, 1.0f);
style.Colors[ImGuiCol_Header] = ImVec4(0.08f, 0.40f, 0.50f, 1.0f);
style.Colors[ImGuiCol_HeaderHovered] = ImVec4(0.10f, 0.48f, 0.59f, 1.0f);
style.Colors[ImGuiCol_HeaderActive] = ImVec4(0.07f, 0.35f, 0.45f, 1.0f);
style.Colors[ImGuiCol_Tab] = ImVec4(0.13f, 0.15f, 0.19f, 1.0f);
style.Colors[ImGuiCol_TabSelected] = ImVec4(0.19f, 0.22f, 0.27f, 1.0f);
style.Colors[ImGuiCol_Separator] = ImVec4(0.24f, 0.28f, 0.34f, 1.0f);
style.Colors[ImGuiCol_TableHeaderBg] = ImVec4(0.13f, 0.15f, 0.19f, 1.0f);
style.Colors[ImGuiCol_TableRowBgAlt] = ImVec4(0.125f, 0.143f, 0.177f, 1.0f);
style.ScaleAllSizes(scale);
}
void load_fonts(float scale) {
ImGuiIO& io = ImGui::GetIO();
io.Fonts->Clear();
ImFontConfig config;
config.OversampleH = 2;
config.OversampleV = 2;
config.PixelSnapH = false;
config.RasterizerDensity = 1.0f;
const float size = 15.0f * scale;
if (!io.Fonts->AddFontFromFileTTF(GITREE_ASSET_DIR "/InterVariable.ttf", size, &config))
io.Fonts->AddFontDefault();
static constexpr ImWchar icon_ranges[] = {ICON_MIN_FA, ICON_MAX_16_FA, 0};
ImFontConfig icon_config;
icon_config.MergeMode = true;
icon_config.PixelSnapH = true;
icon_config.GlyphMinAdvanceX = size;
io.Fonts->AddFontFromFileTTF(
GITREE_ASSET_DIR "/fa-solid-900.ttf", size, &icon_config, icon_ranges);
apply_style(scale);
}
bool sidebar_collapse_row(const char* id, const std::string& label, bool default_open, float reserved_width) {
ImGui::PushID(id);
const ImGuiID state_id = ImGui::GetID("collapse_state");
ImGuiStorage* storage = ImGui::GetStateStorage();
bool open = storage->GetBool(state_id, default_open);
const std::string row = std::string(open ? ICON_FA_CHEVRON_DOWN : ICON_FA_CHEVRON_RIGHT) +
" " + label + "##collapse";
if (ImGui::Selectable(row.c_str(), false, ImGuiSelectableFlags_AllowOverlap,
{std::max(ui(24.0f), ImGui::GetContentRegionAvail().x - reserved_width), 0})) {
open = !open;
storage->SetBool(state_id, open);
}
ImGui::PopID();
return open;
}
bool sidebar_section_header(const char* label, int count, const char* add_tooltip, const char* add_notice) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.60f, 0.67f, 0.76f, 1.0f));
const bool open = sidebar_collapse_row(label, label, true, ui(62.0f));
ImGui::PopStyleColor();
ImGui::SameLine(0, ui(5.0f));
ImGui::TextDisabled("%d", count);
ImGui::SameLine(0, ui(7.0f));
ImGui::PushID(label);
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.43f, 0.90f, 0.51f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.25f, 0.72f, 0.35f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {ui(5.0f), ui(1.0f)});
if (ImGui::SmallButton(ICON_FA_PLUS "##add")) g_notice = add_notice;
if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) ImGui::SetTooltip("%s", add_tooltip);
ImGui::PopStyleVar();
ImGui::PopStyleColor(2);
ImGui::PopID();
return open;
}
void section(const char* label, const std::vector<std::string>& items, const char* item_icon,
const char* add_tooltip, const char* add_notice) {
const bool open = sidebar_section_header(label, static_cast<int>(items.size()), add_tooltip, add_notice);
if (open) {
for (const auto& item : items) {
ImGui::Selectable((std::string(" ") + item_icon + " " + item).c_str());
}
}
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 draw_branch_nodes(const BranchNode& parent, const char* branch_icon, const char* root_group_icon,
const std::string& id_path = {}, int depth = 0) {
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_FA_FOLDER;
const bool open = sidebar_collapse_row(id.c_str(), std::string(group_icon) + " " + name, true, 0);
if (open) {
ImGui::Indent(ui(17.0f));
if (node.branch) ImGui::Selectable((std::string(branch_icon) + " " + name + "##branch" + id).c_str());
draw_branch_nodes(node, branch_icon, root_group_icon, id, depth + 1);
ImGui::Unindent(ui(17.0f));
}
} else {
ImGui::Selectable((std::string(branch_icon) + " " + name + "##" + id).c_str());
}
}
}
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) {
const bool open = sidebar_section_header(label, static_cast<int>(branches.size()), add_tooltip, add_notice);
if (open) {
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(17.0f));
draw_branch_nodes(root, branch_icon, group_icon, label);
ImGui::Unindent(ui(17.0f));
}
ImGui::Separator();
}
void draw_sidebar(float width) {
ImGui::BeginChild("sidebar", {width, -ui(28.0f)}, ImGuiChildFlags_Borders);
if (ImGui::Button(ICON_FA_LIST " List", {ui(98), ui(30)})) {}
ImGui::SameLine();
if (ImGui::Button(ICON_FA_LAYER_GROUP " Workspace", {ui(102), ui(30)})) {}
ImGui::TextDisabled(ICON_FA_EYE " VIEWING %d", static_cast<int>(repo().local_branches.size() + repo().remote_branches.size()));
ImGui::SetNextItemWidth(-1);
ImGui::InputTextWithHint("##filter", ICON_FA_MAGNIFYING_GLASS " Search branches...", g_filter.data(), g_filter.size());
ImGui::Spacing();
branch_section(ICON_FA_HOUSE " LOCAL", repo().local_branches, ICON_FA_CODE_BRANCH, ICON_FA_FOLDER,
"Create local branch", "Branch creation is not wired yet");
branch_section(ICON_FA_CLOUD " REMOTE", repo().remote_branches, ICON_FA_CODE_BRANCH, ICON_FA_GLOBE,
"Add remote", "Remote creation is not wired yet");
section(ICON_FA_DIAGRAM_PROJECT " WORKTREES", repo().worktrees, ICON_FA_COMPUTER,
"Add worktree", "Worktree creation is not wired yet");
section(ICON_FA_CUBES " SUBMODULES", repo().submodules, ICON_FA_CUBES,
"Add submodule", "Submodule creation is not wired yet");
ImGui::EndChild();
}
ImU32 graph_lane_color(int lane) {
static constexpr ImU32 colors[] = {
IM_COL32(20, 179, 211, 255), IM_COL32(220, 38, 190, 255),
IM_COL32(244, 54, 74, 255), IM_COL32(255, 92, 28, 255),
IM_COL32(242, 196, 36, 255), IM_COL32(61, 191, 123, 255),
IM_COL32(82, 126, 235, 255),
};
return colors[static_cast<size_t>(std::abs(lane)) % std::size(colors)];
}
void draw_ref_badge(const RefBadge& badge, int index, int lane) {
std::string text;
if (badge.current) text += ICON_FA_CHECK " ";
text += badge.name;
if (badge.kind == RefKind::local) text += std::string(" ") + ICON_FA_HOUSE;
if (badge.worktree) text += std::string(" ") + ICON_FA_COMPUTER;
if (badge.kind == RefKind::remote || badge.upstream) text += std::string(" ") + ICON_FA_CLOUD;
if (badge.kind == RefKind::tag) text += std::string(" ") + ICON_FA_TAG;
const ImVec4 lane_color = ImGui::ColorConvertU32ToFloat4(graph_lane_color(lane));
const ImVec4 color = ImVec4(lane_color.x * 0.42f, lane_color.y * 0.42f, lane_color.z * 0.42f, 1.0f);
ImGui::PushStyleColor(ImGuiCol_Button, color);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(color.x + 0.06f, color.y + 0.06f, color.z + 0.06f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, color);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, ui(3.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {ui(6.0f), ui(2.0f)});
const std::string id = text + "##ref_badge_" + std::to_string(index);
ImGui::Button(id.c_str());
ImGui::PopStyleVar(2);
ImGui::PopStyleColor(3);
}
void draw_graph_cell(int row, const CommitInfo& commit) {
ImDrawList* draw = ImGui::GetWindowDrawList();
ImVec2 p = ImGui::GetCursorScreenPos();
constexpr float row_height = 28.0f;
const float lane_spacing = ui(22.0f);
const float x = p.x + ui(16.0f) + lane_spacing * commit.lane;
const float y = p.y + ImGui::GetTextLineHeight() * 0.5f;
const ImU32 color = graph_lane_color(commit.lane);
if (!commit.refs.empty()) draw->AddLine({p.x, y}, {x, y}, color, ui(2.0f));
for (const git_oid& parent_id : commit.parent_ids) {
const auto parent = std::find_if(repo().commits.begin(), repo().commits.end(), [&parent_id](const CommitInfo& item) {
return git_oid_equal(&item.oid, &parent_id) != 0;
});
if (parent == repo().commits.end()) continue;
const int parent_row = static_cast<int>(std::distance(repo().commits.begin(), parent));
const float parent_x = p.x + ui(16.0f) + lane_spacing * parent->lane;
const float parent_y = y + ui(row_height) * (parent_row - row);
if (parent->lane == commit.lane) {
draw->AddLine({x, y}, {parent_x, parent_y}, color, ui(2.0f));
} else {
const float bend_y = y + std::min(ui(18.0f), (parent_y - y) * 0.5f);
draw->AddBezierCubic({x, y}, {x, bend_y}, {parent_x, bend_y}, {parent_x, parent_y},
graph_lane_color(parent->lane), ui(2.0f));
}
}
const float radius = ui(9.0f);
draw->AddCircleFilled({x, y}, radius + ui(2.0f), color);
draw->AddCircleFilled({x, y}, radius, IM_COL32(238, 243, 246, 255));
const unsigned int texture = g_avatar_cache ? g_avatar_cache->textureFor(commit.email) : 0;
if (texture) {
draw->AddImageRounded(ImTextureRef(static_cast<ImTextureID>(texture)),
{x - radius + ui(1), y - radius + ui(1)}, {x + radius - ui(1), y + radius - ui(1)},
{0, 0}, {1, 1}, IM_COL32_WHITE, radius);
} else {
const ImVec2 icon_size = ImGui::CalcTextSize(ICON_FA_USER);
draw->AddText({x - icon_size.x * 0.5f, y - icon_size.y * 0.5f}, color, ICON_FA_USER);
}
draw->AddCircle({x, y}, radius, IM_COL32(255, 255, 255, 255), 0, ui(1.0f));
ImGui::Dummy({ui(190.0f), ui(row_height - 4.0f)});
}
void draw_commit_table() {
ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
ImGuiTableFlags_ScrollY | ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp;
if (!ImGui::BeginTable("commits", 4, flags, {-1, -1})) return;
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableSetupColumn("BRANCH / TAG", ImGuiTableColumnFlags_WidthFixed, ui(210.0f));
ImGui::TableSetupColumn("GRAPH", ImGuiTableColumnFlags_WidthFixed, ui(210.0f));
ImGui::TableSetupColumn("COMMIT MESSAGE", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("COMMIT DATE / TIME", ImGuiTableColumnFlags_WidthFixed, ui(180.0f));
ImGui::TableHeadersRow();
for (int i = 0; i < static_cast<int>(repo().commits.size()); ++i) {
const auto& commit = repo().commits[i];
if (g_filter[0]) {
const bool message_matches = commit.summary.find(g_filter.data()) != std::string::npos;
const bool ref_matches = std::any_of(commit.refs.begin(), commit.refs.end(), [](const RefBadge& ref) {
return ref.name.find(g_filter.data()) != std::string::npos;
});
if (!message_matches && !ref_matches) continue;
}
ImGui::TableNextRow(0, ui(28.0f));
ImGui::TableSetColumnIndex(0);
std::string row_id = "##commit" + std::to_string(i);
if (ImGui::Selectable(row_id.c_str(), repo().selected_commit == i,
ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap)) repo().selected_commit = i;
ImGui::SameLine(0, ui(3));
for (int ref_index = 0; ref_index < static_cast<int>(commit.refs.size()); ++ref_index) {
if (ref_index > 0) ImGui::SameLine(0, ui(4));
draw_ref_badge(commit.refs[ref_index], i * 1000 + ref_index, commit.lane);
}
ImGui::TableSetColumnIndex(1);
draw_graph_cell(i, commit);
ImGui::TableSetColumnIndex(2);
ImGui::TextUnformatted(commit.summary.c_str());
ImGui::TableSetColumnIndex(3);
ImGui::TextDisabled("%s", commit.date.c_str());
}
ImGui::EndTable();
}
void draw_details() {
ImGui::BeginChild("details", {ui(360.0f), -ui(28.0f)}, ImGuiChildFlags_Borders);
if (repo().commits.empty()) {
ImGui::TextDisabled("Select a repository with commits");
ImGui::EndChild();
return;
}
const auto& commit = repo().commits[std::clamp(repo().selected_commit, 0, static_cast<int>(repo().commits.size() - 1))];
ImGui::TextDisabled("COMMIT");
ImGui::SameLine();
ImGui::Text("%s", commit.short_id.c_str());
ImGui::Separator();
ImGui::Dummy({0, 6});
ImGui::PushTextWrapPos();
ImGui::Text("%s", commit.summary.c_str());
ImGui::PopTextWrapPos();
ImGui::Dummy({0, 26});
ImGui::Separator();
ImGui::TextColored(ImVec4(0.25f, 0.80f, 0.86f, 1), "%s", commit.author.c_str());
ImGui::TextDisabled("%s", commit.email.c_str());
ImGui::TextDisabled("authored %s", commit.date.c_str());
ImGui::Dummy({0, 24});
ImGui::TextColored(ImVec4(0.95f, 0.68f, 0.22f, 1), "%d parent%s", commit.parents, commit.parents == 1 ? "" : "s");
ImGui::Separator();
ImGui::TextDisabled("Changed files and diffs will appear here");
ImGui::EndChild();
}
bool toolbar_action(const char* id, const char* icon, const char* tooltip) {
ImGui::PushID(id);
const bool clicked = ImGui::Button(icon, {ui(36), ui(36)});
if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) {
ImGui::BeginTooltip();
ImGui::TextUnformatted(tooltip);
ImGui::EndTooltip();
}
ImGui::PopID();
return clicked;
}
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::open_path(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::SetWindowFontScale(1.35f);
ImGui::TextUnformatted("Gitree");
ImGui::SetWindowFontScale(1.0f);
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;
ImGui::TextUnformatted("Gitree");
ImGui::SameLine();
ImGui::TextDisabled("Creative Commons Attribution-ShareAlike 4.0");
ImGui::TextDisabled("Third-party software included with Gitree:");
ImGui::Spacing();
if (ImGui::BeginTable("dependency_licenses", 2,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit,
{ui(680), ui(270)})) {
ImGui::TableSetupColumn("Dependency", ImGuiTableColumnFlags_WidthFixed, ui(170));
ImGui::TableSetupColumn("License and source", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableHeadersRow();
struct Dependency { const char* name; const char* license; const char* url; };
constexpr Dependency dependencies[] = {
{"libgit2 1.9.4", "GPL-2.0 with linking exception", "https://github.com/libgit2/libgit2"},
{"Dear ImGui 1.92.8", "MIT License", "https://github.com/ocornut/imgui"},
{"GLFW 3.4", "zlib/libpng License", "https://github.com/glfw/glfw"},
{"iZo 0.1.0", "MIT License", "https://dock-it.dev/Idea-Studios/iZo"},
{"Inter", "SIL Open Font License 1.1", "https://github.com/rsms/inter"},
{"Font Awesome Free", "SIL Open Font License 1.1", "https://github.com/FortAwesome/Font-Awesome"},
};
for (const auto& dependency : dependencies) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::TextUnformatted(dependency.name);
ImGui::TableSetColumnIndex(1);
ImGui::TextDisabled("%s", dependency.license);
external_link(dependency.url, dependency.url);
}
ImGui::EndTable();
}
ImGui::Spacing();
ImGui::TextDisabled("Complete license texts are included in the source tree with each dependency.");
if (ImGui::Button("Back", {ui(90), 0})) {
g_about_popup = true;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Close", {ui(90), 0})) ImGui::CloseCurrentPopup();
ImGui::EndPopup();
}
void draw_popups() {
if (g_init_popup) { ImGui::OpenPopup("Create repository"); g_init_popup = false; }
if (ImGui::BeginPopupModal("Create repository", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::TextUnformatted("Repository folder");
ImGui::SetNextItemWidth(ui(520.0f));
const bool enter = ImGui::InputText("##path", g_path.data(), g_path.size(), ImGuiInputTextFlags_EnterReturnsTrue);
if (enter || ImGui::Button("Create", {ui(90), 0})) {
if (init_repository(g_path.data())) ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel", {ui(90), 0})) ImGui::CloseCurrentPopup();
if (!g_notice.empty()) ImGui::TextColored(ImVec4(0.96f, 0.55f, 0.35f, 1), "%s", g_notice.c_str());
ImGui::EndPopup();
}
draw_about_popup();
draw_licenses_popup();
}
void draw_footer() {
ImGui::Separator();
ImGui::TextDisabled("%s", g_notice.empty() ? "Ready" : g_notice.c_str());
const char* version = "Gitree " GITREE_VERSION;
ImGui::SameLine(ImGui::GetWindowWidth() - ImGui::CalcTextSize(version).x - ui(18.0f));
ImGui::TextDisabled("%s", version);
}
void draw_new_tab() {
ImGui::BeginChild("new_tab", {-1, -ui(28.0f)}, ImGuiChildFlags_Borders);
ImGui::Dummy({0, ui(36)});
ImGui::SetCursorPosX(ui(42));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.38f, 0.88f, 0.94f, 1.0f));
ImGui::SetWindowFontScale(1.45f);
ImGui::TextUnformatted(ICON_FA_CODE_BRANCH " New Tab");
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor();
ImGui::SetCursorPosX(ui(42));
ImGui::TextDisabled("Open a repository or return to a recently closed tab.");
ImGui::SetCursorPosX(ui(42));
if (ImGui::Button(ICON_FA_FOLDER_OPEN " Open repository", {ui(190), ui(38)}))
pick_and_open_repository();
ImGui::Dummy({0, ui(24)});
ImGui::SetCursorPosX(ui(42));
ImGui::TextUnformatted("RECENTLY CLOSED");
ImGui::SetCursorPosX(ui(42));
ImGui::BeginChild("recently_closed", {-ui(42), ui(300)}, ImGuiChildFlags_Borders);
const auto& recent = g_user_data->recentlyClosed();
if (recent.empty()) {
ImGui::TextDisabled("No recently closed repositories yet.");
} else {
for (int i = 0; i < static_cast<int>(recent.size()); ++i) {
const std::filesystem::path path(recent[i]);
std::string name = path.filename().string();
if (name.empty()) name = path.parent_path().filename().string();
ImGui::PushID(i);
if (ImGui::Selectable((std::string(ICON_FA_CLOCK_ROTATE_LEFT) + " " + name).c_str(), false,
ImGuiSelectableFlags_AllowDoubleClick, {0, ui(42)})) {
open_repository(recent[i].c_str());
}
ImGui::SameLine(ui(260));
ImGui::TextDisabled("%s", recent[i].c_str());
ImGui::PopID();
}
}
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);
if (ImGui::BeginMenuBar()) {
if (ImGui::BeginMenu("File")) {
if (ImGui::MenuItem("Open repository...", "Ctrl+O")) pick_and_open_repository();
if (ImGui::MenuItem("Create repository...", "Ctrl+N")) g_init_popup = 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")) { ImGui::MenuItem("Refresh", "F5"); ImGui::EndMenu(); }
if (ImGui::BeginMenu("Help")) {
if (ImGui::MenuItem("About Gitree")) g_about_popup = true;
ImGui::EndMenu();
}
ImGui::EndMenuBar();
}
size_t tab_to_close = g_tabs.size();
bool add_tab = false;
if (ImGui::BeginTabBar("repositories", ImGuiTabBarFlags_AutoSelectNewTabs | ImGuiTabBarFlags_Reorderable)) {
for (size_t i = 0; i < g_tabs.size(); ++i) {
bool open = true;
const std::string label = g_tabs[i]->name + "###repo_tab_" + std::to_string(i);
const ImGuiTabItemFlags flags = i == g_active_tab ? ImGuiTabItemFlags_SetSelected : ImGuiTabItemFlags_None;
if (ImGui::BeginTabItem(label.c_str(), &open, flags)) {
g_active_tab = i;
ImGui::EndTabItem();
}
if (!open) tab_to_close = i;
}
ImGui::SameLine();
if (ImGui::SmallButton(ICON_FA_PLUS "##new_tab")) add_tab = true;
ImGui::EndTabBar();
}
if (tab_to_close < g_tabs.size()) close_tab(tab_to_close);
if (add_tab) create_new_tab();
ImGui::Separator();
if (!repo().repo) {
draw_new_tab();
draw_footer();
draw_popups();
ImGui::End();
return;
}
ImGui::BeginChild("repo_toolbar", {-1, ui(56)}, ImGuiChildFlags_Borders);
ImGui::TextDisabled("repository");
ImGui::SameLine(ui(145)); ImGui::TextDisabled("branch");
ImGui::SetCursorPosY(ui(26));
ImGui::Text("%s", repo().name.c_str());
ImGui::SameLine(ui(145));
ImGui::SetNextItemWidth(ui(180));
if (ImGui::BeginCombo("##branch", repo().branch.c_str())) {
for (const auto& branch : repo().local_branches) ImGui::Selectable(branch.c_str(), branch == repo().branch);
ImGui::EndCombo();
}
constexpr int action_count = 6;
const float action_size = ui(36.0f);
const float action_spacing = ui(7.0f);
const float action_group_width = action_size * action_count + action_spacing * (action_count - 1);
const float centered_x = (ImGui::GetWindowWidth() - action_group_width) * 0.5f;
ImGui::SetCursorPos({std::max(ui(350.0f), centered_x), ui(10.0f)});
if (toolbar_action("pull", ICON_FA_DOWNLOAD, "Pull from remote")) g_notice = "Pull is not wired yet";
ImGui::SameLine(0, action_spacing);
if (toolbar_action("push", ICON_FA_UPLOAD, "Push to remote")) g_notice = "Push is not wired yet";
ImGui::SameLine(0, action_spacing);
if (toolbar_action("branch", ICON_FA_CODE_BRANCH, "Create branch")) g_notice = "Branch creation is not wired yet";
ImGui::SameLine(0, action_spacing);
if (toolbar_action("stash", ICON_FA_BOX_ARCHIVE, "Stash changes")) g_notice = "Stash is not wired yet";
ImGui::SameLine(0, action_spacing);
if (toolbar_action("pop", ICON_FA_BOX_OPEN, "Pop latest stash")) g_notice = "Stash pop is not wired yet";
ImGui::SameLine(0, action_spacing);
if (toolbar_action("refresh", ICON_FA_ROTATE, "Refresh repository"))
g_git_manager->reload(repo(), g_notice);
ImGui::EndChild();
draw_sidebar(ui(g_sidebar_width));
ImGui::SameLine(0, 0);
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(36, 178, 205, 255) : IM_COL32(62, 72, 88, 255),
ui(1.0f));
ImGui::SameLine(0, 0);
const float detail_width = ui(368.0f);
ImGui::BeginChild("center", {ImGui::GetContentRegionAvail().x - detail_width, -24.0f}, false);
draw_commit_table();
ImGui::EndChild();
ImGui::SameLine();
draw_details();
draw_footer();
draw_popups();
ImGui::End();
}
} // namespace
int runGitree(int argc, char** argv) {
GitManager git_manager;
g_git_manager = &git_manager;
UserData user_data;
g_user_data = &user_data;
g_sidebar_width = user_data.sidebarWidth();
create_new_tab();
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;
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
ImGui::GetIO().IniFilename = user_data.imguiIniPath().c_str();
load_fonts(window_manager.dpiScale());
ImGui_ImplGlfw_InitForOpenGL(window_manager.nativeWindow(), true);
ImGui_ImplOpenGL3_Init("#version 330");
if (argc > 1) open_repository(argv[1]);
while (!window_manager.shouldClose()) {
window_manager.pollEvents();
if (window_manager.consumeDpiChange()) load_fonts(window_manager.dpiScale());
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
avatar_cache.update();
draw_app();
ImGui::Render();
int width = 0, height = 0;
glfwGetFramebufferSize(window_manager.nativeWindow(), &width, &height);
glViewport(0, 0, width, height);
glClearColor(0.10f, 0.116f, 0.145f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
window_manager.swapBuffers();
}
avatar_cache.shutdown();
g_avatar_cache = nullptr;
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
user_data.setSidebarWidth(g_sidebar_width);
user_data.save();
ImGui::DestroyContext();
g_window_manager = nullptr;
g_tabs.clear();
g_user_data = nullptr;
g_git_manager = nullptr;
return 0;
}

3
src/ui/gitree_ui.h Normal file
View File

@@ -0,0 +1,3 @@
#pragma once
int runGitree(int argc, char** argv);