feat(app): scaffold native Git client interface

This commit is contained in:
2026-06-18 16:39:39 -05:00
parent 531ce7ea04
commit 22bcd1dcab
16 changed files with 2561 additions and 1 deletions

6
.gitignore vendored
View File

@@ -32,3 +32,9 @@
*.out
*.app
# Build output and editor state
/build/
/.vs/
/.vscode/
/imgui.ini

12
.gitmodules vendored Normal file
View File

@@ -0,0 +1,12 @@
[submodule "vendor/libgit2"]
path = vendor/libgit2
url = https://github.com/libgit2/libgit2.git
[submodule "vendor/imgui"]
path = vendor/imgui
url = https://github.com/ocornut/imgui.git
[submodule "vendor/glfw"]
path = vendor/glfw
url = https://github.com/glfw/glfw.git
[submodule "vendor/iZo"]
path = vendor/iZo
url = https://dock-it.dev/Idea-Studios/iZo

72
CMakeLists.txt Normal file
View File

@@ -0,0 +1,72 @@
cmake_minimum_required(VERSION 3.21)
project(Gitree VERSION 0.1.0 LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build all dependencies as static libraries" FORCE)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vendor/imgui/imgui.cpp")
message(FATAL_ERROR "Vendor dependencies are missing. Run: git submodule update --init --recursive")
endif()
# GLFW
set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(GLFW_INSTALL OFF CACHE BOOL "" FORCE)
add_subdirectory(vendor/glfw EXCLUDE_FROM_ALL)
# Dear ImGui does not ship a CMake target, so keep its static-library recipe here.
add_library(imgui STATIC
vendor/imgui/imgui.cpp
vendor/imgui/imgui_draw.cpp
vendor/imgui/imgui_tables.cpp
vendor/imgui/imgui_widgets.cpp
vendor/imgui/backends/imgui_impl_glfw.cpp
vendor/imgui/backends/imgui_impl_opengl3.cpp
)
target_include_directories(imgui PUBLIC vendor/imgui vendor/imgui/backends)
target_link_libraries(imgui PUBLIC glfw)
# libgit2
set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(BUILD_CLI OFF CACHE BOOL "" FORCE)
set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(BUILD_FUZZERS OFF CACHE BOOL "" FORCE)
set(USE_SSH OFF CACHE STRING "" FORCE)
if(WIN32)
set(USE_HTTPS WinHTTP CACHE STRING "" FORCE)
endif()
add_subdirectory(vendor/libgit2 EXCLUDE_FROM_ALL)
# Native platform file and folder dialogs.
set(IZO_BUILD_EXAMPLE OFF CACHE BOOL "" FORCE)
add_subdirectory(vendor/iZo EXCLUDE_FROM_ALL)
find_package(OpenGL REQUIRED)
add_executable(gitree WIN32
src/main.cpp
src/window_manager.cpp
src/window_manager.h
)
target_include_directories(gitree PRIVATE 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}"
GITREE_ASSET_DIR="${CMAKE_CURRENT_SOURCE_DIR}/vendor/fonts"
$<$<PLATFORM_ID:Windows>:NOMINMAX;WIN32_LEAN_AND_MEAN>
)
if(WIN32)
target_link_libraries(gitree PRIVATE dwmapi)
endif()
if(MSVC)
target_compile_options(gitree PRIVATE /W4 /permissive-)
else()
target_compile_options(gitree PRIVATE -Wall -Wextra -Wpedantic)
endif()

View File

@@ -1,3 +1,30 @@
# Gitree
An ImGui Based Git GUI git client.
A native Dear ImGui Git client built with libgit2 and GLFW.
## Build on Windows
Clone with submodules, then run `run.bat`. The script configures a Release build,
builds Gitree and launches it. You can pass a repository path to open it directly:
```bat
run.bat C:\path\to\repository
```
Manual build:
```bat
git submodule update --init --recursive
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel
```
Vendored dependencies are pinned under `vendor/` and compiled as static libraries:
- libgit2 1.9.4
- Dear ImGui 1.92.8 (docking branch)
- GLFW 3.4
- iZo 0.1.0
The bundled Inter variable font is licensed under the SIL Open Font License.
Font Awesome Free icons are bundled from Attascii and merged into the ImGui font atlas.

28
run.bat Normal file
View File

@@ -0,0 +1,28 @@
@echo off
setlocal
cd /d "%~dp0"
if not exist vendor\imgui\imgui.cpp (
echo Initializing vendor dependencies...
git submodule update --init --recursive || exit /b 1
)
where ninja >nul 2>nul
if %errorlevel%==0 (
set "GENERATOR=-G Ninja"
) else (
set "GENERATOR="
)
cmake -S . -B build %GENERATOR% -DCMAKE_BUILD_TYPE=Release || exit /b 1
cmake --build build --config Release --parallel || exit /b 1
if exist build\bin\gitree.exe (
start "" build\bin\gitree.exe %*
) else if exist build\bin\Release\gitree.exe (
start "" build\bin\Release\gitree.exe %*
) else (
echo Build succeeded, but gitree.exe was not found.
exit /b 1
)

779
src/main.cpp Normal file
View File

@@ -0,0 +1,779 @@
#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 "window_manager.h"
#include <algorithm>
#include <array>
#include <chrono>
#include <cstdio>
#include <filesystem>
#include <map>
#include <set>
#include <string>
#include <vector>
namespace {
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;
std::vector<RefBadge> refs;
};
struct RepositoryView {
git_repository* repo = nullptr;
std::string path;
std::string name = "No repository";
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() { close(); }
void close() {
if (repo) git_repository_free(repo);
repo = nullptr;
}
};
RepositoryView g_repo;
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;
float ui(float value) { return value * g_ui_scale; }
std::string last_error(const char* fallback) {
const git_error* error = git_error_last();
return error && error->message ? error->message : fallback;
}
std::string format_time(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 read_branches(git_branch_t type, std::vector<std::string>& output) {
git_branch_iterator* iterator = nullptr;
if (git_branch_iterator_new(&iterator, g_repo.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 add_badge(const git_oid* oid, RefBadge badge) {
if (!oid) return;
const auto commit = std::find_if(g_repo.commits.begin(), g_repo.commits.end(),
[oid](const CommitInfo& item) { return git_oid_equal(&item.oid, oid) != 0; });
if (commit != g_repo.commits.end()) commit->refs.push_back(std::move(badge));
}
void load_ref_badges() {
for (const git_branch_t type : {GIT_BRANCH_LOCAL, GIT_BRANCH_REMOTE}) {
git_branch_iterator* iterator = nullptr;
if (git_branch_iterator_new(&iterator, g_repo.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 == g_repo.branch;
badge.worktree = type == GIT_BRANCH_LOCAL &&
(badge.current || g_repo.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) {
add_badge(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, g_repo.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* shorthand = git_reference_shorthand(reference);
add_badge(git_object_id(object), {shorthand ? shorthand : "tag", RefKind::tag});
git_object_free(object);
}
git_reference_free(reference);
}
git_reference_iterator_free(tags);
}
int submodule_callback(git_submodule* submodule, const char*, void* payload) {
auto* names = static_cast<std::vector<std::string>*>(payload);
names->emplace_back(git_submodule_name(submodule));
return 0;
}
void load_repository_data() {
g_repo.local_branches.clear();
g_repo.remote_branches.clear();
g_repo.remotes.clear();
g_repo.worktrees.clear();
g_repo.worktree_branches.clear();
g_repo.submodules.clear();
g_repo.commits.clear();
g_repo.selected_commit = 0;
const char* workdir = git_repository_workdir(g_repo.repo);
const char* repo_path = workdir ? workdir : git_repository_path(g_repo.repo);
g_repo.path = repo_path ? repo_path : "";
std::filesystem::path p(g_repo.path);
g_repo.name = p.filename().string();
if (g_repo.name.empty()) g_repo.name = p.parent_path().filename().string();
git_reference* head = nullptr;
if (git_repository_head(&head, g_repo.repo) == 0) {
const char* shorthand = git_reference_shorthand(head);
if (shorthand) g_repo.branch = shorthand;
git_reference_free(head);
}
read_branches(GIT_BRANCH_LOCAL, g_repo.local_branches);
read_branches(GIT_BRANCH_REMOTE, g_repo.remote_branches);
git_strarray names{};
if (git_remote_list(&names, g_repo.repo) == 0) {
for (size_t i = 0; i < names.count; ++i) g_repo.remotes.emplace_back(names.strings[i]);
git_strarray_dispose(&names);
}
if (git_worktree_list(&names, g_repo.repo) == 0) {
for (size_t i = 0; i < names.count; ++i) {
g_repo.worktrees.emplace_back(names.strings[i]);
git_worktree* worktree = nullptr;
git_repository* worktree_repo = nullptr;
git_reference* worktree_head = nullptr;
if (git_worktree_lookup(&worktree, g_repo.repo, names.strings[i]) == 0 &&
git_repository_open_from_worktree(&worktree_repo, worktree) == 0 &&
git_repository_head(&worktree_head, worktree_repo) == 0) {
const char* branch = git_reference_shorthand(worktree_head);
if (branch) g_repo.worktree_branches.emplace(branch);
}
git_reference_free(worktree_head);
git_repository_free(worktree_repo);
git_worktree_free(worktree);
}
git_strarray_dispose(&names);
}
git_submodule_foreach(g_repo.repo, submodule_callback, &g_repo.submodules);
git_revwalk* walk = nullptr;
if (git_revwalk_new(&walk, g_repo.repo) != 0) return;
git_revwalk_sorting(walk, GIT_SORT_TOPOLOGICAL | GIT_SORT_TIME);
if (git_revwalk_push_head(walk) != 0) {
git_revwalk_free(walk);
return;
}
git_oid oid{};
while (g_repo.commits.size() < 250 && git_revwalk_next(&oid, walk) == 0) {
git_commit* commit = nullptr;
if (git_commit_lookup(&commit, g_repo.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);
const char* summary = git_commit_summary(commit);
item.summary = summary ? summary : "(no commit message)";
const git_signature* author = git_commit_author(commit);
if (author) {
item.author = author->name ? author->name : "Unknown";
item.email = author->email ? author->email : "";
item.date = format_time(author->when.time, author->when.offset);
}
item.parents = static_cast<int>(git_commit_parentcount(commit));
g_repo.commits.push_back(std::move(item));
git_commit_free(commit);
}
git_revwalk_free(walk);
load_ref_badges();
}
bool open_repository(const char* path) {
git_repository* opened = nullptr;
if (git_repository_open_ext(&opened, path, 0, nullptr) != 0) {
g_notice = last_error("Unable to open repository");
return false;
}
g_repo.close();
g_repo.repo = opened;
load_repository_data();
g_notice = "Opened " + g_repo.name;
return true;
}
void pick_and_open_repository() {
izo::dialog_options options;
options.title = "Open Git repository";
if (!g_repo.path.empty()) options.initial_path = g_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());
open_repository(path.c_str());
}
bool init_repository(const char* path) {
git_repository* created = nullptr;
if (git_repository_init(&created, path, 0) != 0) {
g_notice = last_error("Unable to initialize repository");
return false;
}
git_repository_free(created);
return open_repository(path);
}
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);
}
void section(const char* label, const std::vector<std::string>& items) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.60f, 0.67f, 0.76f, 1.0f));
bool open = ImGui::TreeNodeEx(label, ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_SpanAvailWidth);
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 18.0f);
ImGui::TextDisabled("%d", static_cast<int>(items.size()));
ImGui::PopStyleColor();
if (open) {
for (const auto& item : items) ImGui::Selectable((" " + item).c_str());
ImGui::TreePop();
}
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 std::string& id_path = {}) {
for (const auto& [name, node] : parent.children) {
const std::string id = id_path + "/" + name;
if (!node.children.empty()) {
const std::string label = std::string(ICON_FA_FOLDER) + " " + name + "##" + id;
const bool open = ImGui::TreeNodeEx(label.c_str(), ImGuiTreeNodeFlags_DefaultOpen |
ImGuiTreeNodeFlags_SpanAvailWidth);
if (open) {
if (node.branch) ImGui::Selectable((std::string(ICON_FA_CODE_BRANCH) + " " + name + "##branch" + id).c_str());
draw_branch_nodes(node, id);
ImGui::TreePop();
}
} else {
ImGui::Selectable((std::string(ICON_FA_CODE_BRANCH) + " " + name + "##" + id).c_str());
}
}
}
void branch_section(const char* label, const std::vector<std::string>& branches) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.60f, 0.67f, 0.76f, 1.0f));
const bool open = ImGui::TreeNodeEx(label, ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_SpanAvailWidth);
ImGui::SameLine(ImGui::GetContentRegionAvail().x - ui(18.0f));
ImGui::TextDisabled("%d", static_cast<int>(branches.size()));
ImGui::PopStyleColor();
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);
}
draw_branch_nodes(root, label);
ImGui::TreePop();
}
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("VIEWING %d", static_cast<int>(g_repo.local_branches.size() + g_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", g_repo.local_branches);
branch_section(ICON_FA_CLOUD " REMOTE", g_repo.remote_branches);
section(ICON_FA_DIAGRAM_PROJECT " WORKTREES", g_repo.worktrees);
section(ICON_FA_CUBES " SUBMODULES", g_repo.submodules);
ImGui::EndChild();
}
void draw_ref_badge(const RefBadge& badge, int index) {
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 color = badge.kind == RefKind::tag
? ImVec4(0.42f, 0.22f, 0.62f, 1.0f)
: badge.kind == RefKind::remote
? ImVec4(0.10f, 0.31f, 0.35f, 1.0f)
: ImVec4(0.08f, 0.40f, 0.50f, 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, int parents) {
ImDrawList* draw = ImGui::GetWindowDrawList();
ImVec2 p = ImGui::GetCursorScreenPos();
const float x = p.x + ui(28.0f) + ((parents > 1 || row % 7 == 4) ? ui(18.0f) : 0.0f);
const float y = p.y + ImGui::GetTextLineHeight() * 0.5f;
const ImU32 cyan = IM_COL32(16, 178, 214, 255);
draw->AddLine({x, p.y - ui(4.0f)}, {x, p.y + ImGui::GetTextLineHeight() + ui(5.0f)}, cyan, ui(2.0f));
if (parents > 1) draw->AddBezierCubic({x, y}, {x + ui(28), y}, {x + ui(28), y + ui(16)}, {x + ui(28), y + ui(25)}, IM_COL32(153, 55, 220, 255), ui(2.0f));
draw->AddCircleFilled({x, y}, ui(5.0f), row == 0 ? IM_COL32(240, 247, 250, 255) : cyan);
draw->AddCircle({x, y}, ui(7.0f), cyan, 0, ui(2.0f));
ImGui::Dummy({ui(78.0f), ImGui::GetTextLineHeight()});
}
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(92.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>(g_repo.commits.size()); ++i) {
const auto& commit = g_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();
ImGui::TableSetColumnIndex(0);
std::string row_id = "##commit" + std::to_string(i);
if (ImGui::Selectable(row_id.c_str(), g_repo.selected_commit == i,
ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap)) g_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);
}
ImGui::TableSetColumnIndex(1);
draw_graph_cell(i, commit.parents);
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 (g_repo.commits.empty()) {
ImGui::TextDisabled("Select a repository with commits");
ImGui::EndChild();
return;
}
const auto& commit = g_repo.commits[std::clamp(g_repo.selected_commit, 0, static_cast<int>(g_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();
}
void toolbar_button(const char* label, const char* notice) {
if (ImGui::Button(label, {ui(68), ui(34)})) g_notice = notice;
ImGui::SameLine();
}
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_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();
}
if (ImGui::BeginTabBar("repositories", ImGuiTabBarFlags_AutoSelectNewTabs)) {
if (ImGui::BeginTabItem(g_repo.name.c_str())) { ImGui::EndTabItem(); }
ImGui::SameLine();
if (ImGui::SmallButton("+")) pick_and_open_repository();
ImGui::EndTabBar();
}
ImGui::Separator();
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", g_repo.name.c_str());
ImGui::SameLine(ui(145));
ImGui::SetNextItemWidth(ui(180));
if (ImGui::BeginCombo("##branch", g_repo.branch.c_str())) {
for (const auto& branch : g_repo.local_branches) ImGui::Selectable(branch.c_str(), branch == g_repo.branch);
ImGui::EndCombo();
}
ImGui::SameLine(ui(350));
toolbar_button(ICON_FA_DOWNLOAD " Pull", "Pull is not wired yet");
toolbar_button(ICON_FA_UPLOAD " Push", "Push is not wired yet");
toolbar_button(ICON_FA_CODE_BRANCH " Branch", "Branch creation is not wired yet");
toolbar_button(ICON_FA_BOX_ARCHIVE " Stash", "Stash is not wired yet");
toolbar_button(ICON_FA_BOX_OPEN " Pop", "Stash pop is not wired yet");
if (ImGui::Button(ICON_FA_ROTATE " Refresh", {ui(82), ui(34)}) && g_repo.repo) load_repository_data();
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();
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 - 18.0f);
ImGui::TextDisabled("%s", version);
draw_popups();
ImGui::End();
}
} // namespace
int main(int argc, char** argv) {
git_libgit2_init();
WindowManager window_manager;
if (!window_manager.create("Gitree", 1500, 900)) return 1;
g_window_manager = &window_manager;
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
load_fonts(window_manager.dpiScale());
ImGui_ImplGlfw_InitForOpenGL(window_manager.nativeWindow(), true);
ImGui_ImplOpenGL3_Init("#version 330");
if (argc > 1) open_repository(argv[1]);
else open_repository(".");
while (!window_manager.shouldClose()) {
window_manager.pollEvents();
if (window_manager.consumeDpiChange()) load_fonts(window_manager.dpiScale());
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
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();
}
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
g_window_manager = nullptr;
g_repo.close();
git_libgit2_shutdown();
return 0;
}

93
src/window_manager.cpp Normal file
View File

@@ -0,0 +1,93 @@
#include "window_manager.h"
#include <GLFW/glfw3.h>
#include <algorithm>
#ifdef _WIN32
#define GLFW_EXPOSE_NATIVE_WIN32
#include <GLFW/glfw3native.h>
#include <dwmapi.h>
#include <windows.h>
#endif
WindowManager::~WindowManager() {
destroy();
}
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"));
if (set_dpi_awareness) set_dpi_awareness(reinterpret_cast<HANDLE>(-4)); // Per-monitor v2
#endif
if (!glfwInit()) return false;
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_SCALE_TO_MONITOR, GLFW_TRUE);
window_ = glfwCreateWindow(width, height, title, nullptr, nullptr);
if (!window_) {
glfwTerminate();
return false;
}
glfwMakeContextCurrent(window_);
glfwSwapInterval(1);
glfwSetWindowUserPointer(window_, this);
glfwSetWindowContentScaleCallback(window_, contentScaleCallback);
float x_scale = 1.0f;
float y_scale = 1.0f;
glfwGetWindowContentScale(window_, &x_scale, &y_scale);
dpi_scale_ = std::max(x_scale, y_scale);
applyNativeCaption();
return true;
}
void WindowManager::destroy() {
if (!window_) return;
glfwDestroyWindow(window_);
window_ = nullptr;
glfwTerminate();
}
void WindowManager::pollEvents() { glfwPollEvents(); }
void WindowManager::swapBuffers() { glfwSwapBuffers(window_); }
void WindowManager::requestClose() { glfwSetWindowShouldClose(window_, GLFW_TRUE); }
bool WindowManager::shouldClose() const { return !window_ || glfwWindowShouldClose(window_); }
bool WindowManager::consumeDpiChange() {
const bool changed = dpi_changed_;
dpi_changed_ = false;
return changed;
}
void WindowManager::contentScaleCallback(GLFWwindow* window, float x_scale, float y_scale) {
auto* manager = static_cast<WindowManager*>(glfwGetWindowUserPointer(window));
if (manager) manager->updateDpi(std::max(x_scale, y_scale));
}
void WindowManager::updateDpi(float scale) {
scale = std::clamp(scale, 1.0f, 4.0f);
if (scale == dpi_scale_) return;
dpi_scale_ = scale;
dpi_changed_ = true;
applyNativeCaption();
}
void WindowManager::applyNativeCaption() const {
#ifdef _WIN32
const HWND hwnd = glfwGetWin32Window(window_);
const BOOL dark = TRUE;
DwmSetWindowAttribute(hwnd, 20, &dark, sizeof(dark)); // DWMWA_USE_IMMERSIVE_DARK_MODE
// Windows 11 caption customization. Older versions safely ignore these.
const COLORREF caption = RGB(31, 36, 47);
const COLORREF border = RGB(48, 56, 70);
const COLORREF text = RGB(226, 231, 239);
DwmSetWindowAttribute(hwnd, 35, &caption, sizeof(caption)); // DWMWA_CAPTION_COLOR
DwmSetWindowAttribute(hwnd, 34, &border, sizeof(border)); // DWMWA_BORDER_COLOR
DwmSetWindowAttribute(hwnd, 36, &text, sizeof(text)); // DWMWA_TEXT_COLOR
#endif
}

31
src/window_manager.h Normal file
View File

@@ -0,0 +1,31 @@
#pragma once
struct GLFWwindow;
class WindowManager {
public:
WindowManager() = default;
~WindowManager();
WindowManager(const WindowManager&) = delete;
WindowManager& operator=(const WindowManager&) = delete;
bool create(const char* title, int width, int height);
void destroy();
void pollEvents();
void swapBuffers();
void requestClose();
[[nodiscard]] bool shouldClose() const;
[[nodiscard]] GLFWwindow* nativeWindow() const { return window_; }
[[nodiscard]] float dpiScale() const { return dpi_scale_; }
bool consumeDpiChange();
private:
static void contentScaleCallback(GLFWwindow* window, float x_scale, float y_scale);
void updateDpi(float scale);
void applyNativeCaption() const;
GLFWwindow* window_ = nullptr;
float dpi_scale_ = 1.0f;
bool dpi_changed_ = false;
};

BIN
vendor/fonts/InterVariable.ttf vendored Normal file

Binary file not shown.

92
vendor/fonts/LICENSE-Inter.txt vendored Normal file
View File

@@ -0,0 +1,92 @@
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

BIN
vendor/fonts/fa-solid-900.ttf vendored Normal file

Binary file not shown.

1
vendor/glfw vendored Submodule

Submodule vendor/glfw added at 7b6aead9fb

1
vendor/iZo vendored Submodule

Submodule vendor/iZo added at 88eb3ced5b

1416
vendor/icons/IconsFontAwesome6.h vendored Normal file

File diff suppressed because it is too large Load Diff

1
vendor/imgui vendored Submodule

Submodule vendor/imgui added at b61e56346a

1
vendor/libgit2 vendored Submodule

Submodule vendor/libgit2 added at f7164261c9