Compare commits
30 Commits
release-6e
...
release-f0
| Author | SHA1 | Date | |
|---|---|---|---|
| f0fce51efc | |||
| c9ff53c77b | |||
| bec13359a8 | |||
| 120965f507 | |||
| 3e4529204e | |||
| 6c245468b9 | |||
| b85c2b75b6 | |||
| f614199ce5 | |||
| 9424c7b830 | |||
| e4a8e5d8c9 | |||
| 0cc7f7151a | |||
| bf3a50c0e5 | |||
| a087fe429b | |||
| 139e721fcc | |||
| 96b595b27c | |||
| ca6968ae5e | |||
| d7fddcb728 | |||
| 6059945771 | |||
| 181736c0c8 | |||
| 0e08a8a190 | |||
| e68065a4e2 | |||
| 069d4e341f | |||
| ce81922ebb | |||
| ac9df86ef0 | |||
| 1f800c3cef | |||
| 2ca9c6bf77 | |||
| 7a39b4aa20 | |||
| 0fea9ab8a4 | |||
| d338023e59 | |||
| cb145fe722 |
BIN
.gitea/images/gitree_banner.png
Normal file
BIN
.gitea/images/gitree_banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 782 KiB |
BIN
.gitea/images/gitree_banner_rounded.png
Normal file
BIN
.gitea/images/gitree_banner_rounded.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 820 KiB |
BIN
.gitea/images/gitree_logo.png
Normal file
BIN
.gitea/images/gitree_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 794 KiB |
BIN
.gitea/images/gitree_logo_no_bg.png
Normal file
BIN
.gitea/images/gitree_logo_no_bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 123 KiB |
BIN
.gitea/images/id-engine.jpg
Normal file
BIN
.gitea/images/id-engine.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@@ -10,7 +10,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build-windows:
|
||||
name: Build Windows x64
|
||||
name: Build Windows x64_86
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -20,6 +20,12 @@ jobs:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Sync submodules
|
||||
run: |
|
||||
git submodule sync --recursive
|
||||
git submodule update --init --force --recursive
|
||||
git submodule status --recursive
|
||||
|
||||
- name: Generate build names
|
||||
env:
|
||||
GITEA_SHA: ${{ gitea.sha }}
|
||||
@@ -27,8 +33,8 @@ jobs:
|
||||
short_sha="$(printf '%s' "$GITEA_SHA" | cut -c1-7)"
|
||||
|
||||
echo "BUILD_HASH=${short_sha}" >> "$GITHUB_ENV"
|
||||
echo "WINDOWS_EXE=${APP_NAME}-windows-x64-${short_sha}.exe" >> "$GITHUB_ENV"
|
||||
echo "WINDOWS_ARTIFACT=${APP_NAME}-windows-x64-${short_sha}" >> "$GITHUB_ENV"
|
||||
echo "WINDOWS_EXE=${APP_NAME}-windows-x64_86-${short_sha}.exe" >> "$GITHUB_ENV"
|
||||
echo "WINDOWS_ARTIFACT=${APP_NAME}-windows-x64_86-${short_sha}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -119,6 +125,12 @@ jobs:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Sync submodules
|
||||
run: |
|
||||
git submodule sync --recursive
|
||||
git submodule update --init --force --recursive
|
||||
git submodule status --recursive
|
||||
|
||||
- name: Generate build names
|
||||
env:
|
||||
GITEA_SHA: ${{ gitea.sha }}
|
||||
@@ -132,7 +144,10 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y cmake ninja-build curl jq dpkg-dev
|
||||
sudo apt-get install -y \
|
||||
cmake ninja-build curl jq dpkg-dev \
|
||||
libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev \
|
||||
libgl1-mesa-dev
|
||||
|
||||
- name: Configure Linux release build
|
||||
run: |
|
||||
@@ -230,6 +245,12 @@ jobs:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Sync submodules
|
||||
run: |
|
||||
git submodule sync --recursive
|
||||
git submodule update --init --force --recursive
|
||||
git submodule status --recursive
|
||||
|
||||
- name: Generate release names
|
||||
run: |
|
||||
short_sha="$(printf '%s' "$GITEA_SHA" | cut -c1-7)"
|
||||
@@ -238,8 +259,8 @@ jobs:
|
||||
echo "RELEASE_TAG=release-${short_sha}" >> "$GITHUB_ENV"
|
||||
echo "RELEASE_NAME=Release ${short_sha}" >> "$GITHUB_ENV"
|
||||
|
||||
echo "WINDOWS_EXE=${APP_NAME}-windows-x64-${short_sha}.exe" >> "$GITHUB_ENV"
|
||||
echo "WINDOWS_ARTIFACT=${APP_NAME}-windows-x64-${short_sha}" >> "$GITHUB_ENV"
|
||||
echo "WINDOWS_EXE=${APP_NAME}-windows-x64_86-${short_sha}.exe" >> "$GITHUB_ENV"
|
||||
echo "WINDOWS_ARTIFACT=${APP_NAME}-windows-x64_86-${short_sha}" >> "$GITHUB_ENV"
|
||||
|
||||
echo "LINUX_DEB=${APP_NAME}-deb-x64-${short_sha}.deb" >> "$GITHUB_ENV"
|
||||
echo "LINUX_ARTIFACT=${APP_NAME}-deb-x64-${short_sha}" >> "$GITHUB_ENV"
|
||||
@@ -318,7 +339,7 @@ jobs:
|
||||
if [ "$WINDOWS_RESULT" = "success" ]; then
|
||||
echo "- ${WINDOWS_EXE}"
|
||||
else
|
||||
echo "- Windows x64 failed"
|
||||
echo "- Windows x64_86 failed"
|
||||
fi
|
||||
|
||||
if [ "$LINUX_RESULT" = "success" ]; then
|
||||
@@ -398,4 +419,4 @@ jobs:
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-F "attachment=@${asset}" \
|
||||
"${api}/releases/${release_id}/assets?name=${asset_name}"
|
||||
done
|
||||
done
|
||||
|
||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -10,6 +10,6 @@
|
||||
[submodule "vendor/iZo"]
|
||||
path = vendor/iZo
|
||||
url = https://dock-it.dev/Idea-Studios/iZo
|
||||
[submodule "vendor/iKv"]
|
||||
path = vendor/iKv
|
||||
url = https://dock-it.dev/Idea-Studios/iKv
|
||||
[submodule "vendor/iKvxx"]
|
||||
path = vendor/iKvxx
|
||||
url = https://dock-it.dev/Idea-Studios/iKvxx
|
||||
|
||||
@@ -17,6 +17,10 @@ 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)
|
||||
if(UNIX AND NOT APPLE)
|
||||
set(GLFW_BUILD_X11 ON CACHE BOOL "" FORCE)
|
||||
set(GLFW_BUILD_WAYLAND OFF CACHE BOOL "" FORCE)
|
||||
endif()
|
||||
add_subdirectory(vendor/glfw EXCLUDE_FROM_ALL)
|
||||
|
||||
# Dear ImGui does not ship a CMake target, so keep its static-library recipe here.
|
||||
@@ -46,11 +50,9 @@ add_subdirectory(vendor/libgit2 EXCLUDE_FROM_ALL)
|
||||
set(IZO_BUILD_EXAMPLE OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(vendor/iZo EXCLUDE_FROM_ALL)
|
||||
|
||||
# Persistent application settings and session data.
|
||||
set(IKV_BUILD_DEMOS OFF CACHE BOOL "" FORCE)
|
||||
set(IKV_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(IKV_INSTALL OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(vendor/iKv EXCLUDE_FROM_ALL)
|
||||
# Persistent application settings and session data (C++17 bindings for iKv).
|
||||
set(IKVXX_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(vendor/iKvxx EXCLUDE_FROM_ALL)
|
||||
|
||||
find_package(OpenGL REQUIRED)
|
||||
|
||||
@@ -77,7 +79,7 @@ add_executable(gitree WIN32
|
||||
src/models/repository.h
|
||||
)
|
||||
target_include_directories(gitree PRIVATE src vendor/libgit2/include vendor/icons)
|
||||
target_link_libraries(gitree PRIVATE imgui libgit2package iZo::izo ikv::ikv OpenGL::GL)
|
||||
target_link_libraries(gitree PRIVATE imgui libgit2package iZo::izo ikvxx::ikvxx OpenGL::GL)
|
||||
target_compile_definitions(gitree PRIVATE
|
||||
GITREE_VERSION="${PROJECT_VERSION}"
|
||||
GITREE_ASSET_DIR="${CMAKE_CURRENT_SOURCE_DIR}/vendor/fonts"
|
||||
@@ -90,7 +92,7 @@ if(WIN32)
|
||||
endif()
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(gitree PRIVATE /W4 /permissive-)
|
||||
target_compile_options(gitree PRIVATE /W4 /permissive- /MP)
|
||||
else()
|
||||
target_compile_options(gitree PRIVATE -Wall -Wextra -Wpedantic)
|
||||
endif()
|
||||
|
||||
14
README.md
14
README.md
@@ -1,8 +1,8 @@
|
||||
# Gitree
|
||||

|
||||
|
||||
[](https://dock-it.dev/Idea-Studios/Gitree/actions?workflow=windows-build.yml)
|
||||
[](https://dock-it.dev/Idea-Studios/Gitree/releases/latest)
|
||||
[](https://dock-it.dev/Idea-Studios/Gitree/releases)
|
||||
[](https://dock-it.dev/Idea-Studios/Gitree/releases/latest)
|
||||
[](https://en.cppreference.com/w/cpp/20)
|
||||
[](https://dock-it.dev/Idea-Studios/Gitree/src/branch/prod/LICENSE)
|
||||
|
||||
@@ -10,7 +10,7 @@ A fast, native Git desktop client for Windows.
|
||||
|
||||
## Download
|
||||
|
||||
Download the current Windows x64 executable from the
|
||||
Download the current Windows x64_86 executable or Linux x64 DEB from the
|
||||
[latest release](https://dock-it.dev/Idea-Studios/Gitree/releases/latest), or browse
|
||||
[all releases](https://dock-it.dev/Idea-Studios/Gitree/releases).
|
||||
|
||||
@@ -22,7 +22,7 @@ Open Gitree, choose **Open repository**, and select a local Git repository. You
|
||||
also pass its path when launching from a terminal:
|
||||
|
||||
```powershell
|
||||
.\Gitree-windows-x64.exe C:\path\to\repository
|
||||
.\Gitree-windows-x64_86.exe C:\path\to\repository
|
||||
```
|
||||
|
||||
Use the sidebar to switch branches and browse refs. Select a commit to inspect its
|
||||
@@ -32,7 +32,7 @@ details and changes. The commit area lets you stage files and create commits.
|
||||
|
||||
### Requirements
|
||||
|
||||
- Windows 10 or later (x64)
|
||||
- Windows 10 or later (x64_86)
|
||||
- [Git](https://git-scm.com/download/win)
|
||||
- [CMake 3.21 or later](https://cmake.org/download/)
|
||||
- A C++20 compiler: Visual Studio 2022 with the **Desktop development with C++**
|
||||
@@ -91,3 +91,7 @@ cmake --build build --parallel
|
||||
|
||||
Gitree is licensed under the [Creative Commons Attribution-ShareAlike 4.0
|
||||
International license](https://dock-it.dev/Idea-Studios/Gitree/src/branch/prod/LICENSE).
|
||||
|
||||
## Powered by Idea Studios
|
||||
|
||||

|
||||
|
||||
6
run.bat
6
run.bat
@@ -14,8 +14,10 @@ if %errorlevel%==0 (
|
||||
set "GENERATOR="
|
||||
)
|
||||
|
||||
cmake -S . -B build %GENERATOR% -DCMAKE_BUILD_TYPE=Release || exit /b 1
|
||||
cmake --build build --config Release --parallel || exit /b 1
|
||||
if not exist build\CMakeCache.txt (
|
||||
cmake -S . -B build %GENERATOR% -DCMAKE_BUILD_TYPE=Release || exit /b 1
|
||||
)
|
||||
cmake --build build --config Release --parallel %NUMBER_OF_PROCESSORS% || exit /b 1
|
||||
|
||||
if exist build\bin\gitree.exe (
|
||||
start "" build\bin\gitree.exe %*
|
||||
|
||||
@@ -123,7 +123,6 @@ namespace
|
||||
return origin != remotes.end() ? *origin : remotes.front();
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
std::string encodeBase64(std::string_view value)
|
||||
{
|
||||
static constexpr std::string_view alphabet =
|
||||
@@ -143,17 +142,18 @@ namespace
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
#endif
|
||||
|
||||
std::vector<std::string> withAuthOverrideArguments(std::vector<std::string> arguments,
|
||||
const std::optional<GitAuthOverride> &auth)
|
||||
{
|
||||
arguments.insert(arguments.begin(), {
|
||||
"-c", "credential.interactive=never",
|
||||
});
|
||||
if (!auth || auth->username.empty())
|
||||
return arguments;
|
||||
const std::string header = "http.extraHeader=Authorization: Basic " +
|
||||
encodeBase64(auth->username + ":" + auth->password);
|
||||
arguments.insert(arguments.begin(), {
|
||||
"-c", "credential.interactive=never",
|
||||
arguments.insert(arguments.begin() + 2, {
|
||||
"-c", header,
|
||||
});
|
||||
return arguments;
|
||||
@@ -931,6 +931,8 @@ bool GitManager::captureGit(RepositoryView &repository, const std::vector<std::s
|
||||
}
|
||||
#else
|
||||
(void)arguments;
|
||||
(void)command_output;
|
||||
(void)stdin_data;
|
||||
error = "Git commands are currently supported on Windows";
|
||||
return false;
|
||||
#endif
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
#include "user_data.h"
|
||||
|
||||
#include <izo/Paths.hpp>
|
||||
|
||||
extern "C"
|
||||
{
|
||||
#include <ikv.h>
|
||||
}
|
||||
#include <ikvxx/ikvxx.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
@@ -21,6 +17,8 @@ extern "C"
|
||||
|
||||
namespace
|
||||
{
|
||||
using ArrayIndex = ikv::Value::ArrayIndex;
|
||||
|
||||
std::filesystem::path roaming_directory()
|
||||
{
|
||||
if (const auto config = izo::GetKnownPath(izo::KnownPath::Config); !config.empty())
|
||||
@@ -30,12 +28,6 @@ namespace
|
||||
return std::filesystem::temp_directory_path();
|
||||
}
|
||||
|
||||
const ikv_node_t *object_value(const ikv_node_t *object, const char *key, ikv_type_t type)
|
||||
{
|
||||
const ikv_node_t *value = object ? ikv_object_get(object, key) : nullptr;
|
||||
return value && ikv_node_type(value) == type ? value : nullptr;
|
||||
}
|
||||
|
||||
std::optional<std::pair<std::string, std::string>> splitCredentialPayload(const std::string &payload)
|
||||
{
|
||||
const size_t separator = payload.find('\n');
|
||||
@@ -118,7 +110,87 @@ namespace
|
||||
return plain;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string normalizePath(const std::string &path)
|
||||
{
|
||||
if (path.empty())
|
||||
return {};
|
||||
try
|
||||
{
|
||||
std::filesystem::path p(path);
|
||||
std::string result = p.lexically_normal().generic_string();
|
||||
if (result.size() > 1 && result.back() == '/')
|
||||
result.pop_back();
|
||||
return result;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<float> readFloat(const ikv::Value &value)
|
||||
{
|
||||
if (value.isDouble())
|
||||
return static_cast<float>(value.asDouble());
|
||||
if (value.isInt())
|
||||
return static_cast<float>(value.asInt64());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Load settings from an ikv::Value "settings" object into UserData fields.
|
||||
// Returns true if anything useful was read.
|
||||
void loadSettings(const ikv::Value &settings, float &sidebar_width, float &details_width,
|
||||
bool &sidebar_collapsed, float &commit_message_height,
|
||||
float &working_composer_height, int &pull_mode, int &zoom_percent,
|
||||
std::array<float, 4> &sidebar_section_heights,
|
||||
std::array<bool, 4> &sidebar_section_open,
|
||||
std::array<float, 4> &commit_table_column_widths)
|
||||
{
|
||||
if (settings.isMember("sidebar_width"))
|
||||
if (const auto value = readFloat(settings["sidebar_width"]); value)
|
||||
sidebar_width = *value;
|
||||
if (settings.isMember("details_width"))
|
||||
if (const auto value = readFloat(settings["details_width"]); value)
|
||||
details_width = *value;
|
||||
if (settings.isMember("sidebar_collapsed") && settings["sidebar_collapsed"].isBool())
|
||||
sidebar_collapsed = settings["sidebar_collapsed"].asBool();
|
||||
if (settings.isMember("commit_message_height"))
|
||||
if (const auto value = readFloat(settings["commit_message_height"]); value)
|
||||
commit_message_height = *value;
|
||||
if (settings.isMember("working_composer_height"))
|
||||
if (const auto value = readFloat(settings["working_composer_height"]); value)
|
||||
working_composer_height = *value;
|
||||
if (settings.isMember("pull_mode") && settings["pull_mode"].isInt())
|
||||
pull_mode = static_cast<int>(settings["pull_mode"].asInt64());
|
||||
if (settings.isMember("zoom_percent") && settings["zoom_percent"].isInt())
|
||||
zoom_percent = static_cast<int>(settings["zoom_percent"].asInt64());
|
||||
|
||||
if (settings.isMember("sidebar_sections") && settings["sidebar_sections"].isArray())
|
||||
{
|
||||
const ikv::Value &arr = settings["sidebar_sections"];
|
||||
const auto count = std::min(arr.size(), sidebar_section_heights.size());
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
if (const auto value = readFloat(arr[static_cast<ArrayIndex>(i)]); value)
|
||||
sidebar_section_heights[i] = *value;
|
||||
}
|
||||
if (settings.isMember("sidebar_section_open") && settings["sidebar_section_open"].isArray())
|
||||
{
|
||||
const ikv::Value &arr = settings["sidebar_section_open"];
|
||||
const auto count = std::min(arr.size(), sidebar_section_open.size());
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
sidebar_section_open[i] = arr[static_cast<ArrayIndex>(i)].asBool();
|
||||
}
|
||||
if (settings.isMember("commit_table_columns") && settings["commit_table_columns"].isArray())
|
||||
{
|
||||
const ikv::Value &arr = settings["commit_table_columns"];
|
||||
const auto count = std::min(arr.size(), commit_table_column_widths.size());
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
if (const auto value = readFloat(arr[static_cast<ArrayIndex>(i)]); value)
|
||||
commit_table_column_widths[i] = *value;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
UserData::UserData()
|
||||
{
|
||||
@@ -144,240 +216,188 @@ std::string UserData::credentialScope(const std::string &remote_url)
|
||||
|
||||
std::filesystem::path UserData::dataPath() const
|
||||
{
|
||||
return directory_ / "user_data.ikv2b";
|
||||
return directory_ / "user_data.ikv";
|
||||
}
|
||||
|
||||
void UserData::removeLegacyFiles() const
|
||||
{
|
||||
std::error_code error;
|
||||
for (const char *name : {"imgui.ini", "settings.ini", "history.txt", "session.txt", "user_data.ikv"})
|
||||
for (const char *name : {"imgui.ini", "settings.ini", "history.txt", "session.txt",
|
||||
"user_data.ikv2b", "user_data.ikv2"})
|
||||
std::filesystem::remove(directory_ / name, error);
|
||||
}
|
||||
|
||||
UserData::RepoSettings &UserData::repoSettings(const std::string &normalized_path)
|
||||
{
|
||||
return repo_settings_[normalized_path];
|
||||
}
|
||||
|
||||
const UserData::RepoSettings *UserData::findRepoSettings(const std::string &normalized_path) const
|
||||
{
|
||||
const auto it = repo_settings_.find(normalized_path);
|
||||
return it != repo_settings_.end() ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
void UserData::load()
|
||||
{
|
||||
const std::filesystem::path binary_path = dataPath();
|
||||
if (ikv_node_t *root = ikvb_parse_file(binary_path.string().c_str()))
|
||||
|
||||
// ── Primary load: iKvxx binary (.ikv) ────────────────────────────────────
|
||||
try
|
||||
{
|
||||
if (const ikv_node_t *settings = object_value(root, "settings", IKV_OBJECT))
|
||||
const ikv::Value root = [&]() -> ikv::Value
|
||||
{
|
||||
if (const ikv_node_t *value = object_value(settings, "sidebar_width", IKV_FLOAT))
|
||||
sidebar_width_ = static_cast<float>(ikv_as_float(value));
|
||||
if (const ikv_node_t *value = object_value(settings, "details_width", IKV_FLOAT))
|
||||
details_width_ = static_cast<float>(ikv_as_float(value));
|
||||
if (const ikv_node_t *value = object_value(settings, "sidebar_collapsed", IKV_BOOL))
|
||||
sidebar_collapsed_ = ikv_as_bool(value) != 0;
|
||||
if (const ikv_node_t *value = object_value(settings, "pull_mode", IKV_INT))
|
||||
pull_mode_ = static_cast<int>(ikv_as_int(value));
|
||||
if (const ikv_node_t *value = object_value(settings, "zoom_percent", IKV_INT))
|
||||
zoom_percent_ = static_cast<int>(ikv_as_int(value));
|
||||
if (const ikv_node_t *heights = object_value(settings, "sidebar_sections", IKV_ARRAY))
|
||||
try
|
||||
{
|
||||
const uint32_t count = std::min<uint32_t>(
|
||||
ikv_array_size(heights), static_cast<uint32_t>(sidebar_section_heights_.size()));
|
||||
for (uint32_t index = 0; index < count; ++index)
|
||||
sidebar_section_heights_[index] = static_cast<float>(ikv_as_float(ikv_array_get(heights, index)));
|
||||
return ikv::Value::load(binary_path.string());
|
||||
}
|
||||
if (const ikv_node_t *open = object_value(settings, "sidebar_section_open", IKV_ARRAY))
|
||||
catch (const ikv::Error &)
|
||||
{
|
||||
const uint32_t count = std::min<uint32_t>(
|
||||
ikv_array_size(open), static_cast<uint32_t>(sidebar_section_open_.size()));
|
||||
for (uint32_t index = 0; index < count; ++index)
|
||||
sidebar_section_open_[index] = ikv_as_bool(ikv_array_get(open, index)) != 0;
|
||||
}
|
||||
if (const ikv_node_t *widths = object_value(settings, "commit_table_columns", IKV_ARRAY))
|
||||
{
|
||||
const uint32_t count = std::min<uint32_t>(
|
||||
ikv_array_size(widths), static_cast<uint32_t>(commit_table_column_widths_.size()));
|
||||
for (uint32_t index = 0; index < count; ++index)
|
||||
commit_table_column_widths_[index] = static_cast<float>(ikv_as_float(ikv_array_get(widths, index)));
|
||||
return ikv::Value::loadBinary(binary_path.string());
|
||||
}
|
||||
}();
|
||||
|
||||
if (root.isMember("settings") && root["settings"].isObject())
|
||||
{
|
||||
loadSettings(root["settings"],
|
||||
sidebar_width_, details_width_, sidebar_collapsed_,
|
||||
commit_message_height_, working_composer_height_,
|
||||
pull_mode_, zoom_percent_,
|
||||
sidebar_section_heights_, sidebar_section_open_,
|
||||
commit_table_column_widths_);
|
||||
}
|
||||
if (const ikv_node_t *history = object_value(root, "recently_closed", IKV_ARRAY))
|
||||
|
||||
if (root.isMember("recently_closed") && root["recently_closed"].isArray())
|
||||
{
|
||||
const uint32_t count = std::min<uint32_t>(ikv_array_size(history), 100);
|
||||
for (uint32_t index = 0; index < count; ++index)
|
||||
const ikv::Value &arr = root["recently_closed"];
|
||||
const auto count = std::min<std::size_t>(arr.size(), 100);
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
const char *path = ikv_as_string(ikv_array_get(history, index));
|
||||
if (path && *path)
|
||||
recently_closed_.emplace_back(path);
|
||||
}
|
||||
}
|
||||
if (const ikv_node_t *recent = object_value(root, "recent_repositories", IKV_ARRAY))
|
||||
{
|
||||
const uint32_t count = std::min<uint32_t>(ikv_array_size(recent), 100);
|
||||
for (uint32_t index = 0; index < count; ++index)
|
||||
{
|
||||
const char *path = ikv_as_string(ikv_array_get(recent, index));
|
||||
if (path && *path)
|
||||
recent_repositories_.emplace_back(path);
|
||||
}
|
||||
}
|
||||
if (const ikv_node_t *session = object_value(root, "session", IKV_OBJECT))
|
||||
{
|
||||
if (const ikv_node_t *active = object_value(session, "active_tab", IKV_INT))
|
||||
active_repository_ = static_cast<size_t>(std::max<int64_t>(0, ikv_as_int(active)));
|
||||
if (const ikv_node_t *tabs = object_value(session, "tabs", IKV_ARRAY))
|
||||
{
|
||||
for (uint32_t index = 0; index < ikv_array_size(tabs); ++index)
|
||||
const std::string path = arr[static_cast<ArrayIndex>(i)].asString();
|
||||
if (!path.empty())
|
||||
{
|
||||
const char *path = ikv_as_string(ikv_array_get(tabs, index));
|
||||
open_repositories_.emplace_back(path ? path : "");
|
||||
std::string normalized = normalizePath(path);
|
||||
if (std::find(recently_closed_.begin(), recently_closed_.end(), normalized) == recently_closed_.end())
|
||||
recently_closed_.emplace_back(std::move(normalized));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (const ikv_node_t *credentials = object_value(root, "credentials", IKV_ARRAY))
|
||||
|
||||
if (root.isMember("recent_repositories") && root["recent_repositories"].isArray())
|
||||
{
|
||||
for (uint32_t index = 0; index < ikv_array_size(credentials); ++index)
|
||||
const ikv::Value &arr = root["recent_repositories"];
|
||||
const auto count = std::min<std::size_t>(arr.size(), 100);
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
const ikv_node_t *entry = ikv_array_get(credentials, index);
|
||||
if (ikv_node_type(entry) != IKV_OBJECT)
|
||||
continue;
|
||||
const ikv_node_t *scope = object_value(entry, "scope", IKV_STRING);
|
||||
const ikv_node_t *secret = object_value(entry, "secret", IKV_STRING);
|
||||
if (!scope || !secret)
|
||||
continue;
|
||||
const char *scope_value = ikv_as_string(scope);
|
||||
const char *secret_value = ikv_as_string(secret);
|
||||
if (!scope_value || !secret_value || !*scope_value || !*secret_value)
|
||||
continue;
|
||||
encrypted_credentials_[scope_value] = secret_value;
|
||||
const std::string path = arr[static_cast<ArrayIndex>(i)].asString();
|
||||
if (!path.empty())
|
||||
{
|
||||
std::string normalized = normalizePath(path);
|
||||
if (std::find(recent_repositories_.begin(), recent_repositories_.end(), normalized) == recent_repositories_.end())
|
||||
recent_repositories_.emplace_back(std::move(normalized));
|
||||
}
|
||||
}
|
||||
}
|
||||
ikv_free(root);
|
||||
sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
|
||||
details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
|
||||
pull_mode_ = std::clamp(pull_mode_, 0, 3);
|
||||
zoom_percent_ = std::clamp(zoom_percent_, 80, 200);
|
||||
for (float &height : sidebar_section_heights_)
|
||||
height = std::clamp(height, 42.0f, 500.0f);
|
||||
for (float &width : commit_table_column_widths_)
|
||||
width = std::clamp(width, 0.0f, 1200.0f);
|
||||
|
||||
if (root.isMember("session") && root["session"].isObject())
|
||||
{
|
||||
const ikv::Value &session = root["session"];
|
||||
if (session.isMember("active_tab") && session["active_tab"].isInt())
|
||||
active_repository_ = static_cast<size_t>(std::max<int64_t>(0, session["active_tab"].asInt64()));
|
||||
if (session.isMember("tabs") && session["tabs"].isArray())
|
||||
{
|
||||
const ikv::Value &tabs = session["tabs"];
|
||||
for (std::size_t i = 0; i < tabs.size(); ++i)
|
||||
open_repositories_.emplace_back(tabs[static_cast<ArrayIndex>(i)].asString());
|
||||
}
|
||||
}
|
||||
|
||||
if (root.isMember("repository_settings") && root["repository_settings"].isArray())
|
||||
{
|
||||
const ikv::Value &repos = root["repository_settings"];
|
||||
for (std::size_t i = 0; i < repos.size(); ++i)
|
||||
{
|
||||
const ikv::Value &entry = repos[static_cast<ArrayIndex>(i)];
|
||||
if (!entry.isObject() || !entry.isMember("path"))
|
||||
continue;
|
||||
const std::string path_str = entry["path"].asString();
|
||||
if (path_str.empty())
|
||||
continue;
|
||||
const std::string key = normalizePath(path_str);
|
||||
RepoSettings &rs = repo_settings_[key];
|
||||
// column widths
|
||||
rs.column_widths = commit_table_column_widths_; // start from global default
|
||||
if (entry.isMember("commit_table_columns") && entry["commit_table_columns"].isArray())
|
||||
{
|
||||
const ikv::Value &widths = entry["commit_table_columns"];
|
||||
const auto count = std::min<std::size_t>(widths.size(), 4);
|
||||
for (std::size_t c = 0; c < count; ++c)
|
||||
rs.column_widths[c] = static_cast<float>(widths[static_cast<ArrayIndex>(c)].asDouble());
|
||||
}
|
||||
if (entry.isMember("commit_summary") && entry["commit_summary"].isString())
|
||||
rs.pending_commit_summary = entry["commit_summary"].asString();
|
||||
if (entry.isMember("commit_description") && entry["commit_description"].isString())
|
||||
rs.pending_commit_description = entry["commit_description"].asString();
|
||||
}
|
||||
}
|
||||
|
||||
if (root.isMember("credentials") && root["credentials"].isArray())
|
||||
{
|
||||
const ikv::Value &creds = root["credentials"];
|
||||
for (std::size_t i = 0; i < creds.size(); ++i)
|
||||
{
|
||||
const ikv::Value &entry = creds[static_cast<ArrayIndex>(i)];
|
||||
if (!entry.isObject())
|
||||
continue;
|
||||
if (!entry.isMember("scope") || !entry.isMember("secret"))
|
||||
continue;
|
||||
const std::string scope = entry["scope"].asString();
|
||||
const std::string secret = entry["secret"].asString();
|
||||
if (!scope.empty() && !secret.empty())
|
||||
encrypted_credentials_[scope] = secret;
|
||||
}
|
||||
}
|
||||
|
||||
// Clamp loaded values.
|
||||
sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
|
||||
details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
|
||||
pull_mode_ = std::clamp(pull_mode_, 0, 3);
|
||||
zoom_percent_ = std::clamp(zoom_percent_, 80, 200);
|
||||
commit_message_height_ = std::clamp(commit_message_height_, 60.0f, 600.0f);
|
||||
working_composer_height_ = std::clamp(working_composer_height_, 100.0f, 900.0f);
|
||||
for (float &h : sidebar_section_heights_)
|
||||
h = std::clamp(h, 42.0f, 500.0f);
|
||||
for (float &w : commit_table_column_widths_)
|
||||
w = std::clamp(w, 0.0f, 1200.0f);
|
||||
for (auto &[repo_key, rs] : repo_settings_)
|
||||
for (float &w : rs.column_widths)
|
||||
w = std::clamp(w, 0.0f, 1200.0f);
|
||||
if (open_repositories_.empty())
|
||||
active_repository_ = 0;
|
||||
else
|
||||
active_repository_ = std::min(active_repository_, open_repositories_.size() - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
const std::filesystem::path text_data_path = directory_ / "user_data.ikv";
|
||||
if (ikv_node_t *root = ikv_parse_file(text_data_path.string().c_str()))
|
||||
catch (const ikv::Error &)
|
||||
{
|
||||
if (const ikv_node_t *settings = object_value(root, "settings", IKV_OBJECT))
|
||||
{
|
||||
if (const ikv_node_t *value = object_value(settings, "sidebar_width", IKV_FLOAT))
|
||||
sidebar_width_ = static_cast<float>(ikv_as_float(value));
|
||||
if (const ikv_node_t *value = object_value(settings, "details_width", IKV_FLOAT))
|
||||
details_width_ = static_cast<float>(ikv_as_float(value));
|
||||
if (const ikv_node_t *value = object_value(settings, "sidebar_collapsed", IKV_BOOL))
|
||||
sidebar_collapsed_ = ikv_as_bool(value) != 0;
|
||||
if (const ikv_node_t *value = object_value(settings, "pull_mode", IKV_INT))
|
||||
pull_mode_ = static_cast<int>(ikv_as_int(value));
|
||||
if (const ikv_node_t *value = object_value(settings, "zoom_percent", IKV_INT))
|
||||
zoom_percent_ = static_cast<int>(ikv_as_int(value));
|
||||
if (const ikv_node_t *heights = object_value(settings, "sidebar_sections", IKV_ARRAY))
|
||||
{
|
||||
const uint32_t count = std::min<uint32_t>(
|
||||
ikv_array_size(heights), static_cast<uint32_t>(sidebar_section_heights_.size()));
|
||||
for (uint32_t index = 0; index < count; ++index)
|
||||
sidebar_section_heights_[index] = static_cast<float>(ikv_as_float(ikv_array_get(heights, index)));
|
||||
}
|
||||
if (const ikv_node_t *open = object_value(settings, "sidebar_section_open", IKV_ARRAY))
|
||||
{
|
||||
const uint32_t count = std::min<uint32_t>(
|
||||
ikv_array_size(open), static_cast<uint32_t>(sidebar_section_open_.size()));
|
||||
for (uint32_t index = 0; index < count; ++index)
|
||||
sidebar_section_open_[index] = ikv_as_bool(ikv_array_get(open, index)) != 0;
|
||||
}
|
||||
if (const ikv_node_t *widths = object_value(settings, "commit_table_columns", IKV_ARRAY))
|
||||
{
|
||||
const uint32_t count = std::min<uint32_t>(
|
||||
ikv_array_size(widths), static_cast<uint32_t>(commit_table_column_widths_.size()));
|
||||
for (uint32_t index = 0; index < count; ++index)
|
||||
commit_table_column_widths_[index] = static_cast<float>(ikv_as_float(ikv_array_get(widths, index)));
|
||||
}
|
||||
}
|
||||
if (const ikv_node_t *history = object_value(root, "recently_closed", IKV_ARRAY))
|
||||
{
|
||||
const uint32_t count = std::min<uint32_t>(ikv_array_size(history), 100);
|
||||
for (uint32_t index = 0; index < count; ++index)
|
||||
{
|
||||
const char *path = ikv_as_string(ikv_array_get(history, index));
|
||||
if (path && *path)
|
||||
recently_closed_.emplace_back(path);
|
||||
}
|
||||
}
|
||||
if (const ikv_node_t *recent = object_value(root, "recent_repositories", IKV_ARRAY))
|
||||
{
|
||||
const uint32_t count = std::min<uint32_t>(ikv_array_size(recent), 100);
|
||||
for (uint32_t index = 0; index < count; ++index)
|
||||
{
|
||||
const char *path = ikv_as_string(ikv_array_get(recent, index));
|
||||
if (path && *path)
|
||||
recent_repositories_.emplace_back(path);
|
||||
}
|
||||
}
|
||||
if (const ikv_node_t *session = object_value(root, "session", IKV_OBJECT))
|
||||
{
|
||||
if (const ikv_node_t *active = object_value(session, "active_tab", IKV_INT))
|
||||
active_repository_ = static_cast<size_t>(std::max<int64_t>(0, ikv_as_int(active)));
|
||||
if (const ikv_node_t *tabs = object_value(session, "tabs", IKV_ARRAY))
|
||||
{
|
||||
for (uint32_t index = 0; index < ikv_array_size(tabs); ++index)
|
||||
{
|
||||
const char *path = ikv_as_string(ikv_array_get(tabs, index));
|
||||
open_repositories_.emplace_back(path ? path : "");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (const ikv_node_t *credentials = object_value(root, "credentials", IKV_ARRAY))
|
||||
{
|
||||
for (uint32_t index = 0; index < ikv_array_size(credentials); ++index)
|
||||
{
|
||||
const ikv_node_t *entry = ikv_array_get(credentials, index);
|
||||
if (ikv_node_type(entry) != IKV_OBJECT)
|
||||
continue;
|
||||
const ikv_node_t *scope = object_value(entry, "scope", IKV_STRING);
|
||||
const ikv_node_t *secret = object_value(entry, "secret", IKV_STRING);
|
||||
if (!scope || !secret)
|
||||
continue;
|
||||
const char *scope_value = ikv_as_string(scope);
|
||||
const char *secret_value = ikv_as_string(secret);
|
||||
if (!scope_value || !secret_value || !*scope_value || !*secret_value)
|
||||
continue;
|
||||
encrypted_credentials_[scope_value] = secret_value;
|
||||
}
|
||||
}
|
||||
ikv_free(root);
|
||||
sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
|
||||
details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
|
||||
pull_mode_ = std::clamp(pull_mode_, 0, 3);
|
||||
zoom_percent_ = std::clamp(zoom_percent_, 80, 200);
|
||||
for (float &height : sidebar_section_heights_)
|
||||
height = std::clamp(height, 42.0f, 500.0f);
|
||||
for (float &width : commit_table_column_widths_)
|
||||
width = std::clamp(width, 0.0f, 1200.0f);
|
||||
if (open_repositories_.empty())
|
||||
active_repository_ = 0;
|
||||
else
|
||||
active_repository_ = std::min(active_repository_, open_repositories_.size() - 1);
|
||||
save();
|
||||
return;
|
||||
// File missing or corrupt — try legacy paths below.
|
||||
}
|
||||
|
||||
// Import the original files once when upgrading an existing installation.
|
||||
// ── Legacy text load (user_data.ikv — old text format) ───────────────────
|
||||
// (The old binary was user_data.ikv2b; user_data.ikv was the text file.)
|
||||
// We'll silently skip if missing.
|
||||
const std::filesystem::path text_path = directory_ / "user_data_legacy.ikv";
|
||||
// (No legacy text migration needed — just fall through to defaults.)
|
||||
|
||||
// ── Import from very old settings.ini ────────────────────────────────────
|
||||
std::ifstream settings(directory_ / "settings.ini");
|
||||
std::string key;
|
||||
while (settings >> key)
|
||||
{
|
||||
if (key == "sidebar_width")
|
||||
settings >> sidebar_width_;
|
||||
else if (key == "details_width")
|
||||
settings >> details_width_;
|
||||
else if (key == "pull_mode")
|
||||
settings >> pull_mode_;
|
||||
else if (key == "zoom_percent")
|
||||
settings >> zoom_percent_;
|
||||
if (key == "sidebar_width") settings >> sidebar_width_;
|
||||
else if (key == "details_width") settings >> details_width_;
|
||||
else if (key == "pull_mode") settings >> pull_mode_;
|
||||
else if (key == "zoom_percent") settings >> zoom_percent_;
|
||||
else if (key.rfind("sidebar_section_", 0) == 0)
|
||||
{
|
||||
const size_t index = static_cast<size_t>(std::stoul(key.substr(16)));
|
||||
@@ -387,33 +407,41 @@ void UserData::load()
|
||||
sidebar_section_heights_[index] = height;
|
||||
}
|
||||
}
|
||||
sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
|
||||
details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
|
||||
pull_mode_ = std::clamp(pull_mode_, 0, 3);
|
||||
zoom_percent_ = std::clamp(zoom_percent_, 80, 200);
|
||||
for (float &height : sidebar_section_heights_)
|
||||
height = std::clamp(height, 42.0f, 500.0f);
|
||||
|
||||
std::ifstream history(directory_ / "history.txt");
|
||||
std::string path;
|
||||
while (history >> std::quoted(path))
|
||||
{
|
||||
if (!path.empty())
|
||||
recently_closed_.push_back(path);
|
||||
{
|
||||
std::string normalized = normalizePath(path);
|
||||
if (std::find(recently_closed_.begin(), recently_closed_.end(), normalized) == recently_closed_.end())
|
||||
recently_closed_.push_back(std::move(normalized));
|
||||
}
|
||||
if (recently_closed_.size() == 100)
|
||||
break;
|
||||
}
|
||||
recent_repositories_ = recently_closed_;
|
||||
|
||||
std::ifstream session(directory_ / "session.txt");
|
||||
session >> active_repository_;
|
||||
while (session >> std::quoted(path))
|
||||
open_repositories_.push_back(path);
|
||||
std::ifstream session_file(directory_ / "session.txt");
|
||||
session_file >> active_repository_;
|
||||
while (session_file >> std::quoted(path))
|
||||
{
|
||||
if (!path.empty())
|
||||
open_repositories_.push_back(normalizePath(path));
|
||||
}
|
||||
|
||||
sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
|
||||
details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
|
||||
pull_mode_ = std::clamp(pull_mode_, 0, 3);
|
||||
zoom_percent_ = std::clamp(zoom_percent_, 80, 200);
|
||||
for (float &h : sidebar_section_heights_)
|
||||
h = std::clamp(h, 42.0f, 500.0f);
|
||||
if (open_repositories_.empty())
|
||||
active_repository_ = 0;
|
||||
else
|
||||
active_repository_ = std::min(active_repository_, open_repositories_.size() - 1);
|
||||
save();
|
||||
save(); // migrate to new format immediately
|
||||
}
|
||||
|
||||
std::optional<SavedGitCredential> UserData::remoteCredential(const std::string &remote_url) const
|
||||
@@ -434,12 +462,26 @@ std::optional<SavedGitCredential> UserData::remoteCredential(const std::string &
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string UserData::pendingCommitSummary(const std::string &repo_path) const
|
||||
{
|
||||
const std::string key = normalizePath(repo_path);
|
||||
const RepoSettings *rs = findRepoSettings(key);
|
||||
return rs ? rs->pending_commit_summary : std::string{};
|
||||
}
|
||||
|
||||
std::string UserData::pendingCommitDescription(const std::string &repo_path) const
|
||||
{
|
||||
const std::string key = normalizePath(repo_path);
|
||||
const RepoSettings *rs = findRepoSettings(key);
|
||||
return rs ? rs->pending_commit_description : std::string{};
|
||||
}
|
||||
|
||||
void UserData::addRecentRepository(const std::string &path)
|
||||
{
|
||||
if (path.empty())
|
||||
return;
|
||||
std::erase(recent_repositories_, path);
|
||||
recent_repositories_.insert(recent_repositories_.begin(), path);
|
||||
if (path.empty()) return;
|
||||
const std::string normalized = normalizePath(path);
|
||||
std::erase(recent_repositories_, normalized);
|
||||
recent_repositories_.insert(recent_repositories_.begin(), normalized);
|
||||
if (recent_repositories_.size() > 100)
|
||||
recent_repositories_.resize(100);
|
||||
save();
|
||||
@@ -447,14 +489,14 @@ void UserData::addRecentRepository(const std::string &path)
|
||||
|
||||
void UserData::addRecentlyClosed(const std::string &path)
|
||||
{
|
||||
if (path.empty())
|
||||
return;
|
||||
std::erase(recent_repositories_, path);
|
||||
recent_repositories_.insert(recent_repositories_.begin(), path);
|
||||
if (path.empty()) return;
|
||||
const std::string normalized = normalizePath(path);
|
||||
std::erase(recent_repositories_, normalized);
|
||||
recent_repositories_.insert(recent_repositories_.begin(), normalized);
|
||||
if (recent_repositories_.size() > 100)
|
||||
recent_repositories_.resize(100);
|
||||
std::erase(recently_closed_, path);
|
||||
recently_closed_.insert(recently_closed_.begin(), path);
|
||||
std::erase(recently_closed_, normalized);
|
||||
recently_closed_.insert(recently_closed_.begin(), normalized);
|
||||
if (recently_closed_.size() > 100)
|
||||
recently_closed_.resize(100);
|
||||
save();
|
||||
@@ -462,8 +504,7 @@ void UserData::addRecentlyClosed(const std::string &path)
|
||||
|
||||
std::string UserData::takeRecentlyClosed()
|
||||
{
|
||||
if (recently_closed_.empty())
|
||||
return {};
|
||||
if (recently_closed_.empty()) return {};
|
||||
std::string path = std::move(recently_closed_.front());
|
||||
recently_closed_.erase(recently_closed_.begin());
|
||||
save();
|
||||
@@ -472,6 +513,8 @@ std::string UserData::takeRecentlyClosed()
|
||||
|
||||
void UserData::setRepositorySession(std::vector<std::string> paths, size_t active_repository)
|
||||
{
|
||||
for (auto &path : paths)
|
||||
path = normalizePath(path);
|
||||
open_repositories_ = std::move(paths);
|
||||
active_repository_ = open_repositories_.empty()
|
||||
? 0
|
||||
@@ -483,12 +526,10 @@ void UserData::storeRemoteCredential(const std::string &remote_url, const std::s
|
||||
const std::string &password)
|
||||
{
|
||||
const std::string scope = credentialScope(remote_url);
|
||||
if (scope.empty() || username.empty())
|
||||
return;
|
||||
if (scope.empty() || username.empty()) return;
|
||||
#ifdef _WIN32
|
||||
const auto protected_value = protectCredentialString(username + "\n" + password);
|
||||
if (!protected_value)
|
||||
return;
|
||||
if (!protected_value) return;
|
||||
encrypted_credentials_[scope] = *protected_value;
|
||||
save();
|
||||
#else
|
||||
@@ -502,52 +543,103 @@ void UserData::clearRemoteCredential(const std::string &remote_url)
|
||||
save();
|
||||
}
|
||||
|
||||
void UserData::setPendingCommitSummary(const std::string &repo_path, const std::string &text)
|
||||
{
|
||||
repoSettings(normalizePath(repo_path)).pending_commit_summary = text;
|
||||
// Don't trigger a full save on every keystroke — caller must call save() when appropriate.
|
||||
}
|
||||
|
||||
void UserData::setPendingCommitDescription(const std::string &repo_path, const std::string &text)
|
||||
{
|
||||
repoSettings(normalizePath(repo_path)).pending_commit_description = text;
|
||||
}
|
||||
|
||||
void UserData::save() const
|
||||
{
|
||||
std::filesystem::create_directories(directory_);
|
||||
ikv_node_t *root = ikv_create_object("gitree");
|
||||
if (!root)
|
||||
return;
|
||||
ikv::Value root(ikv::objectValue, "gitree");
|
||||
|
||||
ikv_node_t *settings = ikv_object_add_object(root, "settings");
|
||||
ikv_object_set_float(settings, "sidebar_width", sidebar_width_);
|
||||
ikv_object_set_float(settings, "details_width", details_width_);
|
||||
ikv_object_set_bool(settings, "sidebar_collapsed", sidebar_collapsed_);
|
||||
ikv_object_set_int(settings, "pull_mode", pull_mode_);
|
||||
ikv_object_set_int(settings, "zoom_percent", zoom_percent_);
|
||||
ikv_node_t *heights = ikv_object_add_array(settings, "sidebar_sections", IKV_FLOAT);
|
||||
for (const float height : sidebar_section_heights_)
|
||||
ikv_array_add_float(heights, height);
|
||||
ikv_node_t *open = ikv_object_add_array(settings, "sidebar_section_open", IKV_BOOL);
|
||||
for (const bool state : sidebar_section_open_)
|
||||
ikv_array_add_bool(open, state);
|
||||
ikv_node_t *widths = ikv_object_add_array(settings, "commit_table_columns", IKV_FLOAT);
|
||||
for (const float width : commit_table_column_widths_)
|
||||
ikv_array_add_float(widths, width);
|
||||
auto settings = root.makeObject("settings");
|
||||
settings["sidebar_width"] = static_cast<double>(sidebar_width_);
|
||||
settings["details_width"] = static_cast<double>(details_width_);
|
||||
settings["sidebar_collapsed"] = sidebar_collapsed_;
|
||||
settings["commit_message_height"] = static_cast<double>(commit_message_height_);
|
||||
settings["working_composer_height"] = static_cast<double>(working_composer_height_);
|
||||
settings["pull_mode"] = static_cast<int64_t>(pull_mode_);
|
||||
settings["zoom_percent"] = static_cast<int64_t>(zoom_percent_);
|
||||
|
||||
ikv_node_t *history = ikv_object_add_array(root, "recently_closed", IKV_STRING);
|
||||
for (const auto &path : recently_closed_)
|
||||
ikv_array_add_string(history, path.c_str());
|
||||
auto heights = settings.makeArray("sidebar_sections", ikv::realValue);
|
||||
for (const float h : sidebar_section_heights_)
|
||||
heights.append(static_cast<double>(h));
|
||||
|
||||
ikv_node_t *recent = ikv_object_add_array(root, "recent_repositories", IKV_STRING);
|
||||
for (const auto &path : recent_repositories_)
|
||||
ikv_array_add_string(recent, path.c_str());
|
||||
auto open = settings.makeArray("sidebar_section_open", ikv::booleanValue);
|
||||
for (const bool b : sidebar_section_open_)
|
||||
open.append(b);
|
||||
|
||||
ikv_node_t *session = ikv_object_add_object(root, "session");
|
||||
ikv_object_set_int(session, "active_tab", static_cast<int64_t>(active_repository_));
|
||||
ikv_node_t *tabs = ikv_object_add_array(session, "tabs", IKV_STRING);
|
||||
for (const auto &path : open_repositories_)
|
||||
ikv_array_add_string(tabs, path.c_str());
|
||||
auto col_widths = settings.makeArray("commit_table_columns", ikv::realValue);
|
||||
for (const float w : commit_table_column_widths_)
|
||||
col_widths.append(static_cast<double>(w));
|
||||
|
||||
ikv_node_t *credentials = ikv_object_add_array(root, "credentials", IKV_OBJECT);
|
||||
for (const auto &[scope, secret] : encrypted_credentials_)
|
||||
auto repos = root.makeArray("repository_settings", ikv::objectValue);
|
||||
for (const auto &[repo_path, rs] : repo_settings_)
|
||||
{
|
||||
ikv_node_t *entry = ikv_array_add_object(credentials);
|
||||
ikv_object_set_string(entry, "scope", scope.c_str());
|
||||
ikv_object_set_string(entry, "secret", secret.c_str());
|
||||
auto entry = repos.appendObject();
|
||||
entry["path"] = repo_path;
|
||||
auto rw = entry.makeArray("commit_table_columns", ikv::realValue);
|
||||
for (const float w : rs.column_widths)
|
||||
rw.append(static_cast<double>(w));
|
||||
if (!rs.pending_commit_summary.empty())
|
||||
entry["commit_summary"] = rs.pending_commit_summary;
|
||||
if (!rs.pending_commit_description.empty())
|
||||
entry["commit_description"] = rs.pending_commit_description;
|
||||
}
|
||||
|
||||
ikvb_write_file_version(dataPath().string().c_str(), root, IKV_VERSION_2);
|
||||
ikv_free(root);
|
||||
auto history = root.makeArray("recently_closed", ikv::stringValue);
|
||||
for (const auto &p : recently_closed_)
|
||||
history.append(p);
|
||||
|
||||
auto recent = root.makeArray("recent_repositories", ikv::stringValue);
|
||||
for (const auto &p : recent_repositories_)
|
||||
recent.append(p);
|
||||
|
||||
auto session = root.makeObject("session");
|
||||
session["active_tab"] = static_cast<int64_t>(active_repository_);
|
||||
auto tabs = session.makeArray("tabs", ikv::stringValue);
|
||||
for (const auto &p : open_repositories_)
|
||||
tabs.append(p);
|
||||
|
||||
auto credentials = root.makeArray("credentials", ikv::objectValue);
|
||||
for (const auto &[scope, secret] : encrypted_credentials_)
|
||||
{
|
||||
auto entry = credentials.appendObject();
|
||||
entry["scope"] = scope;
|
||||
entry["secret"] = secret;
|
||||
}
|
||||
|
||||
root.writeBinary(dataPath().string(), ikv::Version::v2);
|
||||
removeLegacyFiles();
|
||||
}
|
||||
|
||||
float UserData::commitTableColumnWidth(const std::string &repo_path, size_t index) const
|
||||
{
|
||||
if (index >= 4) return 0.0f;
|
||||
const std::string normalized = normalizePath(repo_path);
|
||||
const RepoSettings *rs = findRepoSettings(normalized);
|
||||
if (rs) return rs->column_widths[index];
|
||||
return commit_table_column_widths_[index];
|
||||
}
|
||||
|
||||
void UserData::setCommitTableColumnWidth(const std::string &repo_path, size_t index, float width)
|
||||
{
|
||||
if (index >= 4) return;
|
||||
const std::string normalized = normalizePath(repo_path);
|
||||
RepoSettings &rs = repo_settings_[normalized];
|
||||
// Initialise from global defaults if this is a brand-new entry.
|
||||
if (rs.column_widths[0] == 0.0f && rs.column_widths[1] == 0.0f &&
|
||||
rs.column_widths[2] == 0.0f && rs.column_widths[3] == 0.0f)
|
||||
{
|
||||
rs.column_widths = commit_table_column_widths_;
|
||||
}
|
||||
rs.column_widths[index] = width;
|
||||
save();
|
||||
}
|
||||
|
||||
@@ -30,10 +30,14 @@ public:
|
||||
[[nodiscard]] float sidebarSectionHeight(size_t index) const { return sidebar_section_heights_.at(index); }
|
||||
[[nodiscard]] bool sidebarCollapsed() const { return sidebar_collapsed_; }
|
||||
[[nodiscard]] bool sidebarSectionOpen(size_t index) const { return sidebar_section_open_.at(index); }
|
||||
[[nodiscard]] float commitTableColumnWidth(size_t index) const { return commit_table_column_widths_.at(index); }
|
||||
[[nodiscard]] float commitMessageHeight() const { return commit_message_height_; }
|
||||
[[nodiscard]] float workingComposerHeight() const { return working_composer_height_; }
|
||||
[[nodiscard]] float commitTableColumnWidth(const std::string &repo_path, size_t index) const;
|
||||
[[nodiscard]] int pullMode() const { return pull_mode_; }
|
||||
[[nodiscard]] int zoomPercent() const { return zoom_percent_; }
|
||||
[[nodiscard]] std::optional<SavedGitCredential> remoteCredential(const std::string &remote_url) const;
|
||||
[[nodiscard]] std::string pendingCommitSummary(const std::string &repo_path) const;
|
||||
[[nodiscard]] std::string pendingCommitDescription(const std::string &repo_path) const;
|
||||
static std::string credentialScope(const std::string &remote_url);
|
||||
|
||||
void setSidebarWidth(float width) { sidebar_width_ = width; }
|
||||
@@ -41,7 +45,11 @@ public:
|
||||
void setSidebarSectionHeight(size_t index, float height) { sidebar_section_heights_.at(index) = height; }
|
||||
void setSidebarCollapsed(bool collapsed) { sidebar_collapsed_ = collapsed; }
|
||||
void setSidebarSectionOpen(size_t index, bool open) { sidebar_section_open_.at(index) = open; }
|
||||
void setCommitTableColumnWidth(size_t index, float width) { commit_table_column_widths_.at(index) = width; }
|
||||
void setCommitMessageHeight(float height) { commit_message_height_ = height; }
|
||||
void setWorkingComposerHeight(float height) { working_composer_height_ = height; }
|
||||
void setCommitTableColumnWidth(const std::string &repo_path, size_t index, float width);
|
||||
void setPendingCommitSummary(const std::string &repo_path, const std::string &text);
|
||||
void setPendingCommitDescription(const std::string &repo_path, const std::string &text);
|
||||
void setPullMode(int mode)
|
||||
{
|
||||
pull_mode_ = mode;
|
||||
@@ -62,9 +70,18 @@ public:
|
||||
void save() const;
|
||||
|
||||
private:
|
||||
struct RepoSettings
|
||||
{
|
||||
std::array<float, 4> column_widths = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
std::string pending_commit_summary;
|
||||
std::string pending_commit_description;
|
||||
};
|
||||
|
||||
void load();
|
||||
[[nodiscard]] std::filesystem::path dataPath() const;
|
||||
void removeLegacyFiles() const;
|
||||
RepoSettings &repoSettings(const std::string &normalized_path);
|
||||
const RepoSettings *findRepoSettings(const std::string &normalized_path) const;
|
||||
|
||||
std::filesystem::path directory_;
|
||||
std::vector<std::string> recent_repositories_;
|
||||
@@ -76,8 +93,11 @@ private:
|
||||
std::array<float, 4> sidebar_section_heights_ = {110.0f, 220.0f, 90.0f, 150.0f};
|
||||
bool sidebar_collapsed_ = false;
|
||||
std::array<bool, 4> sidebar_section_open_ = {true, true, true, true};
|
||||
float commit_message_height_ = 125.0f;
|
||||
float working_composer_height_ = 320.0f;
|
||||
std::array<float, 4> commit_table_column_widths_ = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
int pull_mode_ = 1;
|
||||
int zoom_percent_ = 100;
|
||||
std::unordered_map<std::string, RepoSettings> repo_settings_;
|
||||
std::unordered_map<std::string, std::string> encrypted_credentials_;
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <iomanip>
|
||||
#include <initializer_list>
|
||||
#include <numeric>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
|
||||
@@ -439,7 +440,11 @@ std::vector<MinimapSegment> buildMinimapSegments(const std::string& text, Syntax
|
||||
if (std::isspace(value)) {
|
||||
size_t end = cursor + 1;
|
||||
while (end < text.size() && std::isspace(static_cast<unsigned char>(text[end]))) ++end;
|
||||
addMinimapSegment(segments, std::max(0.04f, static_cast<float>(end - cursor) * 0.18f), IM_COL32(0, 0, 0, 0));
|
||||
if (segments.empty()) {
|
||||
cursor = end;
|
||||
continue;
|
||||
}
|
||||
addMinimapSegment(segments, static_cast<float>(end - cursor) * 0.22f, IM_COL32(0, 0, 0, 0));
|
||||
cursor = end;
|
||||
continue;
|
||||
}
|
||||
@@ -533,9 +538,9 @@ float minimapTotalUnits(const std::vector<MinimapEntry>& entries) {
|
||||
return std::max(1.0f, total_units);
|
||||
}
|
||||
|
||||
void drawMinimap(const std::vector<MinimapEntry>& entries, const ImVec2& viewport_size, float scale,
|
||||
std::optional<float> drawMinimap(const std::vector<MinimapEntry>& entries, const ImVec2& viewport_size, float scale,
|
||||
float rendered_content_height, float visible_start, float visible_height, float max_scroll_y,
|
||||
float content_height_override = 0.0f) {
|
||||
bool& drag_active, float& drag_offset, float content_height_override = 0.0f) {
|
||||
const ImVec2 minimum = ImGui::GetCursorScreenPos();
|
||||
const float total_units = minimapTotalUnits(entries);
|
||||
const float min_unit_height = scaled(0.72f, scale);
|
||||
@@ -554,7 +559,7 @@ void drawMinimap(const std::vector<MinimapEntry>& entries, const ImVec2& viewpor
|
||||
draw->AddRect(minimum, {minimum.x + viewport_size.x, minimum.y + content_size.y},
|
||||
hovered || active ? IM_COL32(70, 76, 88, 180) : IM_COL32(46, 50, 58, 140), scaled(3.0f, scale));
|
||||
|
||||
if (entries.empty() || content_size.y <= 1.0f) return;
|
||||
if (entries.empty() || content_size.y <= 1.0f) return std::nullopt;
|
||||
|
||||
const float content_left = minimum.x + scaled(5.0f, scale);
|
||||
const float content_right = minimum.x + viewport_size.x - scaled(5.0f, scale);
|
||||
@@ -585,17 +590,38 @@ void drawMinimap(const std::vector<MinimapEntry>& entries, const ImVec2& viewpor
|
||||
const float viewport_height = std::clamp(content_size.y * (clamped_visible_height / rendered_height),
|
||||
scaled(14.0f, scale), content_size.y);
|
||||
const float viewport_y = minimum.y + content_size.y * (clamped_visible_start / rendered_height);
|
||||
if ((hovered || active) && ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
||||
const float mouse_ratio = std::clamp((ImGui::GetIO().MousePos.y - minimum.y) / std::max(1.0f, content_size.y), 0.0f, 1.0f);
|
||||
const float target = std::clamp(mouse_ratio * rendered_height - clamped_visible_height * 0.5f, 0.0f, max_scroll_y);
|
||||
ImGui::SetScrollY(target);
|
||||
const ImVec2 viewport_min{minimum.x + scaled(1.0f, scale), viewport_y};
|
||||
const ImVec2 viewport_max{minimum.x + viewport_size.x - scaled(1.0f, scale), viewport_y + viewport_height};
|
||||
const bool viewport_hovered = ImGui::IsMouseHoveringRect(viewport_min, viewport_max);
|
||||
if (ImGui::IsItemActivated() && ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
||||
if (viewport_hovered) {
|
||||
drag_active = true;
|
||||
drag_offset = ImGui::GetIO().MousePos.y - viewport_y;
|
||||
} else {
|
||||
drag_active = false;
|
||||
const float mouse_ratio = std::clamp((ImGui::GetIO().MousePos.y - minimum.y) / std::max(1.0f, content_size.y), 0.0f, 1.0f);
|
||||
return std::clamp(mouse_ratio * rendered_height - clamped_visible_height * 0.5f, 0.0f, max_scroll_y);
|
||||
}
|
||||
}
|
||||
draw->AddRectFilled({minimum.x + scaled(1.0f, scale), viewport_y},
|
||||
{minimum.x + viewport_size.x - scaled(1.0f, scale), viewport_y + viewport_height},
|
||||
IM_COL32(109, 129, 170, hovered || active ? 44 : 28), scaled(2.0f, scale));
|
||||
draw->AddRect({minimum.x + scaled(1.0f, scale), viewport_y},
|
||||
{minimum.x + viewport_size.x - scaled(1.0f, scale), viewport_y + viewport_height},
|
||||
IM_COL32(120, 138, 170, 120), scaled(2.0f, scale));
|
||||
if (drag_active) {
|
||||
if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
||||
drag_active = false;
|
||||
} else {
|
||||
const float desired_viewport_y = std::clamp(ImGui::GetIO().MousePos.y - drag_offset,
|
||||
minimum.y, minimum.y + content_size.y - viewport_height);
|
||||
const float travel = std::max(1.0f, content_size.y - viewport_height);
|
||||
const float scroll_ratio = (desired_viewport_y - minimum.y) / travel;
|
||||
return std::clamp(scroll_ratio * max_scroll_y, 0.0f, max_scroll_y);
|
||||
}
|
||||
} else if ((hovered || active) && ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
||||
const float mouse_ratio = std::clamp((ImGui::GetIO().MousePos.y - minimum.y) / std::max(1.0f, content_size.y), 0.0f, 1.0f);
|
||||
return std::clamp(mouse_ratio * rendered_height - clamped_visible_height * 0.5f, 0.0f, max_scroll_y);
|
||||
}
|
||||
draw->AddRectFilled(viewport_min, viewport_max,
|
||||
IM_COL32(109, 129, 170, hovered || active || drag_active ? 52 : 28), scaled(2.0f, scale));
|
||||
draw->AddRect(viewport_min, viewport_max,
|
||||
IM_COL32(120, 138, 170, drag_active ? 180 : 120), scaled(2.0f, scale));
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void drawCodeLine(const std::string& text, SyntaxLanguage language, SyntaxState& syntax,
|
||||
@@ -645,31 +671,6 @@ void drawCodeLineNumber(int value, float x, float y, ImU32 color) {
|
||||
ImGui::GetWindowDrawList()->AddText({x, y}, color, text.c_str());
|
||||
}
|
||||
|
||||
std::string ellipsizeText(std::string_view text, float max_width) {
|
||||
if (text.empty() || max_width <= 0.0f) return {};
|
||||
|
||||
std::string display{text};
|
||||
if (ImGui::CalcTextSize(display.c_str()).x <= max_width) return display;
|
||||
|
||||
constexpr const char* overflow = "...";
|
||||
const float overflow_width = ImGui::CalcTextSize(overflow).x;
|
||||
if (overflow_width >= max_width) return overflow;
|
||||
|
||||
while (!display.empty() && ImGui::CalcTextSize(display.c_str()).x + overflow_width > max_width) {
|
||||
display.pop_back();
|
||||
}
|
||||
display += overflow;
|
||||
return display;
|
||||
}
|
||||
|
||||
void drawEllipsizedText(ImDrawList* draw, ImVec2 position, ImU32 color, std::string_view text,
|
||||
float max_width, const ImVec2& clip_min, const ImVec2& clip_max) {
|
||||
const std::string display = ellipsizeText(text, max_width);
|
||||
if (display.empty()) return;
|
||||
draw->PushClipRect(clip_min, clip_max, true);
|
||||
draw->AddText(position, color, display.c_str());
|
||||
draw->PopClipRect();
|
||||
}
|
||||
}
|
||||
|
||||
void DiffViewer::open(RepositoryView& repository, GitManager& manager, const std::string& path,
|
||||
@@ -1078,7 +1079,7 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCac
|
||||
ImGui::TextDisabled("No blame data is available for this file.");
|
||||
} else {
|
||||
SyntaxState syntax;
|
||||
const float info_width = scaled(270.0f, scale);
|
||||
const float info_width = scaled(34.0f, scale);
|
||||
const float line_number_width = scaled(48.0f, scale);
|
||||
const float code_gutter = info_width + line_number_width + scaled(14.0f, scale);
|
||||
for (size_t index = 0; index < blame_lines_.size(); ++index) {
|
||||
@@ -1087,7 +1088,7 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCac
|
||||
const ImVec2 line_minimum = ImGui::GetCursorScreenPos();
|
||||
const ImU32 accent = blameColor(line.hash, line.show_attribution ? 255 : 190);
|
||||
const ImU32 background = line.show_attribution ? IM_COL32(39, 42, 50, 150) : IM_COL32(31, 34, 40, 105);
|
||||
const float blame_row_height = line.show_attribution ? scaled(28.0f, scale) : scaled(21.0f, scale);
|
||||
const float blame_row_height = scaled(21.0f, scale);
|
||||
drawCodeLine(line.text, language, syntax, scale, background, code_gutter, content_width,
|
||||
blame_row_height, wrap_columns);
|
||||
ImDrawList* draw = ImGui::GetWindowDrawList();
|
||||
@@ -1097,8 +1098,8 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCac
|
||||
drawCodeLineNumber(line.line_number, line_minimum.x + info_width + scaled(6.0f, scale),
|
||||
line_minimum.y + scaled(2.0f, scale), IM_COL32(126, 132, 142, 255));
|
||||
if (line.show_attribution) {
|
||||
const float avatar_size = scaled(16.0f, scale);
|
||||
const ImVec2 avatar_min{line_minimum.x + scaled(8.0f, scale), line_minimum.y + scaled(2.0f, scale)};
|
||||
const float avatar_size = scaled(14.0f, scale);
|
||||
const ImVec2 avatar_min{line_minimum.x + scaled(10.0f, scale), line_minimum.y + scaled(3.0f, scale)};
|
||||
const unsigned int avatar_texture = avatars ? avatars->textureFor(line.email) : 0;
|
||||
if (avatar_texture) {
|
||||
draw->AddImageRounded(ImTextureRef(static_cast<ImTextureID>(avatar_texture)),
|
||||
@@ -1108,18 +1109,16 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCac
|
||||
draw->AddRectFilled(avatar_min, {avatar_min.x + avatar_size, avatar_min.y + avatar_size},
|
||||
accent, scaled(3.0f, scale));
|
||||
}
|
||||
const float info_x = avatar_min.x + avatar_size + scaled(8.0f, scale);
|
||||
const float info_right = line_minimum.x + info_width - scaled(10.0f, scale);
|
||||
const float info_text_width = std::max(0.0f, info_right - info_x);
|
||||
const ImVec2 info_clip_min{info_x, line_minimum.y};
|
||||
const ImVec2 info_clip_max{info_right, line_maximum.y};
|
||||
const std::string author = line.author.empty() ? "Unknown author" : line.author;
|
||||
std::string summary = line.summary.empty() ? "(no summary)" : line.summary;
|
||||
if (!line.date.empty()) summary += " " + line.date;
|
||||
drawEllipsizedText(draw, {info_x, line_minimum.y + scaled(1.0f, scale)},
|
||||
IM_COL32(216, 220, 226, 255), author, info_text_width, info_clip_min, info_clip_max);
|
||||
drawEllipsizedText(draw, {info_x, line_minimum.y + scaled(10.0f, scale)},
|
||||
IM_COL32(134, 140, 151, 255), summary, info_text_width, info_clip_min, info_clip_max);
|
||||
if (ImGui::IsMouseHoveringRect(avatar_min, {avatar_min.x + avatar_size, avatar_min.y + avatar_size})) {
|
||||
const std::string author = line.author.empty() ? "Unknown author" : line.author;
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextUnformatted(author.c_str());
|
||||
if (!line.summary.empty())
|
||||
ImGui::TextDisabled("%s", line.summary.c_str());
|
||||
if (!line.date.empty())
|
||||
ImGui::TextDisabled("%s", line.date.c_str());
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
} else {
|
||||
draw->AddText({line_minimum.x + scaled(16.0f, scale), line_minimum.y + scaled(2.0f, scale)},
|
||||
IM_COL32(92, 99, 110, 255), "...");
|
||||
@@ -1191,19 +1190,15 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCac
|
||||
const float minimap_height = std::min(max_minimap_height, minimap_content_height);
|
||||
const float minimap_x = child_minimum.x + child_size.x - scrollbar_width - minimap_width - scaled(4.0f, scale);
|
||||
const float minimap_y = child_minimum.y + scaled(4.0f, scale);
|
||||
const ImVec2 restore_cursor = ImGui::GetCursorScreenPos();
|
||||
ImGui::SetCursorScreenPos({minimap_x, minimap_y});
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, ImVec4(0.28f, 0.31f, 0.37f, 0.55f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, ImVec4(0.36f, 0.40f, 0.47f, 0.72f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, ImVec4(0.44f, 0.49f, 0.57f, 0.86f));
|
||||
if (request_scroll_to_top) ImGui::SetNextWindowScroll({0.0f, 0.0f});
|
||||
ImGui::BeginChild("diff_content_minimap", {minimap_width, minimap_height},
|
||||
ImGuiChildFlags_Borders, ImGuiWindowFlags_NoMove);
|
||||
drawMinimap(minimap_entries, {minimap_width - scaled(1.0f, scale), minimap_height}, scale,
|
||||
rendered_content_height, main_scroll_y, main_window_height, max_scroll_y, minimap_content_height);
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleColor(5);
|
||||
const auto requested_scroll = drawMinimap(minimap_entries,
|
||||
{minimap_width, minimap_height}, scale,
|
||||
rendered_content_height, main_scroll_y, main_window_height, max_scroll_y,
|
||||
minimap_drag_active_, minimap_drag_offset_, minimap_content_height);
|
||||
ImGui::SetCursorScreenPos(restore_cursor);
|
||||
if (requested_scroll)
|
||||
ImGui::SetScrollY(*requested_scroll);
|
||||
}
|
||||
if (use_code_font) ImGui::PopFont();
|
||||
ImGui::EndChild();
|
||||
|
||||
@@ -62,6 +62,8 @@ private:
|
||||
std::vector<HistoryEntry> history_entries_;
|
||||
bool line_wrap_ = false;
|
||||
bool scroll_to_top_ = false;
|
||||
bool minimap_drag_active_ = false;
|
||||
float minimap_drag_offset_ = 0.0f;
|
||||
|
||||
void reload(RepositoryView& repository, GitManager& manager, std::string& notice);
|
||||
void loadSupplement(RepositoryView& repository, GitManager& manager, Mode mode, std::string& notice);
|
||||
|
||||
@@ -131,8 +131,8 @@ 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);
|
||||
constexpr auto active_refresh_interval = std::chrono::milliseconds(500);
|
||||
constexpr auto background_refresh_interval = std::chrono::milliseconds(1500);
|
||||
|
||||
enum class GitAsyncOperation { reload, capture, pull, push, checkout_branch, push_branch, fetch, stash, pop_stash };
|
||||
|
||||
@@ -205,6 +205,11 @@ float combined_ui_scale(float dpi_scale, int zoom_percent) {
|
||||
return std::clamp(dpi_scale + zoom_scale - 1.0f, 0.80f, 4.0f);
|
||||
}
|
||||
|
||||
void remove_stray_imgui_ini() {
|
||||
std::error_code error;
|
||||
std::filesystem::remove("imgui.ini", error);
|
||||
}
|
||||
|
||||
bool text_height_checkbox(const char* label, bool* value) {
|
||||
const ImVec2 padding = ImGui::GetStyle().FramePadding;
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {padding.x, 0.0f});
|
||||
@@ -361,15 +366,16 @@ void transfer_repository_state(RepositoryView& source, RepositoryView& target) {
|
||||
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;
|
||||
// Do NOT copy selected_commit or scroll_to_commit from snapshot — those are live UI state
|
||||
// managed by restore_selected_commit() after this function returns.
|
||||
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 == -1)
|
||||
return "working_tree";
|
||||
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);
|
||||
@@ -377,6 +383,10 @@ std::string selected_commit_hash(const RepositoryView& repository) {
|
||||
|
||||
void restore_selected_commit(RepositoryView& repository, const std::string& hash) {
|
||||
if (hash.empty()) return;
|
||||
if (hash == "working_tree") {
|
||||
repository.selected_commit = -1;
|
||||
return;
|
||||
}
|
||||
git_oid target{};
|
||||
if (git_oid_fromstr(&target, hash.c_str()) != 0) return;
|
||||
for (size_t index = 0; index < repository.commits.size(); ++index) {
|
||||
@@ -831,11 +841,16 @@ void begin_inline_branch(int commit_index) {
|
||||
}
|
||||
|
||||
void persist_repository_session() {
|
||||
if (!g_user_data || g_tabs.empty()) return;
|
||||
if (!g_user_data) 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);
|
||||
size_t active_index = 0;
|
||||
for (size_t i = 0; i < g_tabs.size(); ++i) {
|
||||
if (g_tabs[i]->path.empty()) continue;
|
||||
if (i == g_active_tab) active_index = paths.size();
|
||||
paths.push_back(g_tabs[i]->path);
|
||||
}
|
||||
g_user_data->setRepositorySession(std::move(paths), active_index);
|
||||
}
|
||||
|
||||
void clear_sidebar_filter() {
|
||||
@@ -877,16 +892,40 @@ void reset_repository_view() {
|
||||
g_reset_repository_view = true;
|
||||
}
|
||||
|
||||
void save_pending_commit_draft() {
|
||||
if (!g_user_data || g_tabs.empty() || g_active_tab >= g_tabs.size()) return;
|
||||
const std::string& path = g_tabs[g_active_tab]->path;
|
||||
if (path.empty()) return;
|
||||
g_user_data->setPendingCommitSummary(path, g_commit_summary.data());
|
||||
g_user_data->setPendingCommitDescription(path, g_commit_description.data());
|
||||
g_user_data->save();
|
||||
}
|
||||
|
||||
void restore_pending_commit_draft(size_t tab_index) {
|
||||
if (!g_user_data || tab_index >= g_tabs.size()) return;
|
||||
const std::string& path = g_tabs[tab_index]->path;
|
||||
g_commit_summary.fill('\0');
|
||||
g_commit_description.fill('\0');
|
||||
if (path.empty()) return;
|
||||
const std::string summary = g_user_data->pendingCommitSummary(path);
|
||||
const std::string desc = g_user_data->pendingCommitDescription(path);
|
||||
std::snprintf(g_commit_summary.data(), g_commit_summary.size(), "%s", summary.c_str());
|
||||
std::snprintf(g_commit_description.data(), g_commit_description.size(), "%s", desc.c_str());
|
||||
}
|
||||
|
||||
void activate_repository_tab(size_t index) {
|
||||
if (index >= g_tabs.size() || index == g_active_tab) return;
|
||||
save_pending_commit_draft(); // persist draft of the tab we're leaving
|
||||
g_active_tab = index;
|
||||
g_tab_to_select = g_tabs[index].get();
|
||||
reset_repository_view();
|
||||
restore_pending_commit_draft(index); // load draft for the tab we're entering
|
||||
g_tabs[index]->pending_background_refresh = true;
|
||||
persist_repository_session();
|
||||
}
|
||||
|
||||
void create_new_tab(bool persist = true) {
|
||||
save_pending_commit_draft();
|
||||
g_tabs.push_back(std::make_unique<RepositoryView>());
|
||||
g_active_tab = g_tabs.size() - 1;
|
||||
g_tab_to_select = g_tabs.back().get();
|
||||
@@ -897,6 +936,7 @@ void create_new_tab(bool persist = true) {
|
||||
void close_tab(size_t index) {
|
||||
if (index >= g_tabs.size()) return;
|
||||
const bool closing_active_tab = index == g_active_tab;
|
||||
if (closing_active_tab) save_pending_commit_draft();
|
||||
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);
|
||||
@@ -1296,7 +1336,7 @@ void sidebar_item_context(const std::string& item, SidebarItemKind kind) {
|
||||
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(g_git_manager->worktreePath(repo(), item, error))
|
||||
: std::filesystem::path(repo().path) / item;
|
||||
if (path.empty() || !izo::OpenPath(path, &error)) g_notice = error;
|
||||
}
|
||||
@@ -1656,7 +1696,7 @@ void draw_sidebar(float width) {
|
||||
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,
|
||||
section(ICON_TB_FOLDER_TREE " SUBMODULES", repo().submodules, ICON_TB_LAYERS_LINKED,
|
||||
"Add submodule", "Add submodule", SidebarItemKind::submodule, 3,
|
||||
section_heights[3], 0.0f, minimum_height, section_open.size(), false, false);
|
||||
ImGui::PopStyleVar();
|
||||
@@ -1682,7 +1722,8 @@ float outline_icon_width(const char* icon) {
|
||||
|
||||
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);
|
||||
const float text_scale = 0.82f;
|
||||
float width = ImGui::CalcTextSize(display_name.c_str()).x * text_scale + 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);
|
||||
@@ -1723,7 +1764,7 @@ bool draw_overflow_ref_badge(size_t count, int widget_index, int lane, bool row_
|
||||
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);
|
||||
GraphRenderer::laneColor(lane, alpha), 0.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());
|
||||
@@ -1763,11 +1804,13 @@ void draw_ref_badge(const RefBadge& badge, int widget_index, int commit_index, i
|
||||
? 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);
|
||||
const float rounding = 0.0f;
|
||||
draw->AddRectFilled(minimum, maximum, background, rounding, corners);
|
||||
|
||||
const float text_scale = 0.82f;
|
||||
const float label_width = ImGui::CalcTextSize(display_name.c_str()).x * text_scale;
|
||||
float x = minimum.x + ui(5.0f);
|
||||
const float text_y = minimum.y + (chip_size.y - ImGui::GetFontSize()) * 0.5f;
|
||||
const float text_y = minimum.y + (chip_size.y - ImGui::GetFontSize() * text_scale) * 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) {
|
||||
@@ -1776,8 +1819,13 @@ void draw_ref_badge(const RefBadge& badge, int widget_index, int commit_index, i
|
||||
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);
|
||||
|
||||
// Draw text with smaller font scale
|
||||
ImGui::PushFont(nullptr, ImGui::GetStyle().FontSizeBase * text_scale);
|
||||
draw->AddText(ImGui::GetFont(), ImGui::GetFontSize(), {x, text_y}, color, display_name.c_str());
|
||||
ImGui::PopFont();
|
||||
|
||||
x += label_width + 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);
|
||||
@@ -1954,15 +2002,15 @@ void draw_commit_table() {
|
||||
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)),
|
||||
reference_width = std::clamp(ui(g_user_data->commitTableColumnWidth(repo().path, 0)),
|
||||
ui(120.0f), std::max(ui(120.0f), available_table_width * 0.45f));
|
||||
graph_width = std::clamp(ui(g_user_data->commitTableColumnWidth(1)),
|
||||
graph_width = std::clamp(ui(g_user_data->commitTableColumnWidth(repo().path, 1)),
|
||||
ui(90.0f), std::max(ui(90.0f), available_table_width * 0.35f));
|
||||
date_width = std::clamp(ui(g_user_data->commitTableColumnWidth(3)),
|
||||
date_width = std::clamp(ui(g_user_data->commitTableColumnWidth(repo().path, 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 (g_user_data->commitTableColumnWidth(repo().path, 0) <= 0.0f) reference_width = default_reference_width;
|
||||
if (g_user_data->commitTableColumnWidth(repo().path, 1) <= 0.0f) graph_width = default_graph_width;
|
||||
if (g_user_data->commitTableColumnWidth(repo().path, 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)) /
|
||||
@@ -2020,7 +2068,7 @@ void draw_commit_table() {
|
||||
ImGui::TableHeadersRow();
|
||||
if (g_user_data) {
|
||||
for (int column = 0; column < 4; ++column)
|
||||
g_user_data->setCommitTableColumnWidth(static_cast<size_t>(column),
|
||||
g_user_data->setCommitTableColumnWidth(repo().path, static_cast<size_t>(column),
|
||||
ImGui::GetColumnWidth(column) / g_ui_scale);
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
@@ -2033,16 +2081,17 @@ void draw_commit_table() {
|
||||
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 scrollbar_size = ImGui::GetWindowWidth() - ImGui::GetWindowContentRegionMax().x > 0.0f ? ImGui::GetStyle().ScrollbarSize : 0.0f;
|
||||
const float row_right = message_left + message_width + date_width +
|
||||
ImGui::GetStyle().CellPadding.x * 2.0f;
|
||||
ImGui::GetStyle().CellPadding.x * 2.0f - scrollbar_size;
|
||||
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},
|
||||
draw->PushClipRect({row_left, table_body_clip_top},
|
||||
{row_right, table_body_clip_bottom}, false);
|
||||
draw->AddRectFilled({message_left, row_top}, {row_right, row_top + row_height},
|
||||
draw->AddRectFilled({row_left, row_top}, {row_right, row_top + row_height},
|
||||
selected ? IM_COL32(45, 58, 84, 155) : IM_COL32(70, 77, 90, 55));
|
||||
draw->PopClipRect();
|
||||
}
|
||||
@@ -2062,20 +2111,35 @@ void draw_commit_table() {
|
||||
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)};
|
||||
const float lane_x = position.x + ui(17.0f) + ui(28.0f) * first_lane;
|
||||
const ImVec2 center{lane_x, position.y + ui(12.0f)};
|
||||
const float radius = ui(9.6f);
|
||||
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 start{center.x, center.y + radius};
|
||||
const ImVec2 end{lane_x, next_center_y};
|
||||
const float turn_y = center.y + (end.y - center.y) * 0.55f;
|
||||
const float turn_y = start.y + (end.y - start.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,
|
||||
draw_dotted_bezier(draw, start, {start.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));
|
||||
|
||||
// Draw WIP node as dotted circle using a helper or drawing segmented pieces
|
||||
draw->PushClipRect({position.x, table_body_clip_top},
|
||||
{position.x + graph_width, table_body_clip_bottom}, false);
|
||||
constexpr int segments = 12;
|
||||
const ImU32 circle_color = IM_COL32(23, 181, 204, 220);
|
||||
for (int index = 0; index < segments; index += 2) {
|
||||
const float start_angle = static_cast<float>(index) * (2.0f * 3.14159265f / static_cast<float>(segments));
|
||||
const float end_angle = static_cast<float>(index + 1) * (2.0f * 3.14159265f / static_cast<float>(segments));
|
||||
draw->PathClear();
|
||||
draw->PathArcTo(center, radius, start_angle, end_angle, 6);
|
||||
draw->PathStroke(circle_color, ImDrawFlags_None, ui(1.8f));
|
||||
}
|
||||
draw->PopClipRect();
|
||||
ImGui::Dummy({0, ui(20)});
|
||||
ImGui::TableSetColumnIndex(2);
|
||||
int modified_count = 0;
|
||||
@@ -2318,12 +2382,17 @@ void draw_commit_table() {
|
||||
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());
|
||||
|
||||
// Scale commit message text font 10% smaller (scale 0.90f)
|
||||
ImGui::PushFont(nullptr, ImGui::GetStyle().FontSizeBase * 0.90f);
|
||||
const float smaller_text_y = message_position.y + (row_height - ImGui::GetFontSize()) * 0.5f - ui(1.0f);
|
||||
message_draw->AddText({message_position.x, smaller_text_y}, 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},
|
||||
message_draw->AddText({message_position.x + summary_width + ui(12.0f), smaller_text_y},
|
||||
description_color, commit.description.c_str());
|
||||
}
|
||||
ImGui::PopFont();
|
||||
message_draw->PopClipRect();
|
||||
ImGui::PopID();
|
||||
ImGui::TableSetColumnIndex(3);
|
||||
@@ -2491,7 +2560,7 @@ void draw_file_row(const std::string& path, FileChangeKind kind, int id,
|
||||
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)});
|
||||
ImGui::SetCursorScreenPos({maximum.x - button_width - ui(4.0f), minimum.y + (maximum.y - minimum.y - ui(20.0f)) * 0.5f});
|
||||
row_action_clicked = file_action_button(label, !staged);
|
||||
}
|
||||
if (row_action_clicked) {
|
||||
@@ -2594,6 +2663,7 @@ void draw_files(const std::vector<File>& files, bool staged_filter = false,
|
||||
}
|
||||
|
||||
void draw_working_details() {
|
||||
ImGui::Indent(ui(10.0f));
|
||||
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;
|
||||
@@ -2797,6 +2867,12 @@ void draw_working_details() {
|
||||
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');
|
||||
// Clear persisted draft after a successful commit.
|
||||
if (g_user_data && !repo().path.empty()) {
|
||||
g_user_data->setPendingCommitSummary(repo().path, "");
|
||||
g_user_data->setPendingCommitDescription(repo().path, "");
|
||||
g_user_data->save();
|
||||
}
|
||||
amend = false;
|
||||
}
|
||||
}
|
||||
@@ -2804,9 +2880,11 @@ void draw_working_details() {
|
||||
ImGui::EndDisabled();
|
||||
ImGui::Unindent(ui(8.0f));
|
||||
ImGui::EndChild();
|
||||
ImGui::Unindent(ui(10.0f));
|
||||
}
|
||||
|
||||
void draw_commit_details() {
|
||||
ImGui::Indent(ui(10.0f));
|
||||
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];
|
||||
@@ -2918,6 +2996,7 @@ void draw_commit_details() {
|
||||
? commit.all_files : commit.changed_files;
|
||||
draw_files(displayed_files, false, false, oid_string(commit.oid));
|
||||
ImGui::EndChild();
|
||||
ImGui::Unindent(ui(10.0f));
|
||||
}
|
||||
|
||||
void draw_details(float width) {
|
||||
@@ -2951,7 +3030,7 @@ bool toolbar_action(const char* id, const char* label, const char* icon, const c
|
||||
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 bool raw_clicked = ImGui::InvisibleButton("##action", {ui(logical_width), ui(54.0f)});
|
||||
const ImVec2 minimum = ImGui::GetItemRectMin();
|
||||
const ImVec2 maximum = ImGui::GetItemRectMax();
|
||||
const bool clicked_dropdown = raw_clicked && dropdown &&
|
||||
@@ -2963,7 +3042,7 @@ bool toolbar_action(const char* id, const char* label, const char* icon, const c
|
||||
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 float icon_font_size = ImGui::GetFontSize() * 1.62f;
|
||||
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);
|
||||
@@ -2971,20 +3050,20 @@ bool toolbar_action(const char* id, const char* label, const char* icon, const c
|
||||
? 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)},
|
||||
{minimum.x + (maximum.x - minimum.x - label_size.x) * 0.5f, minimum.y + ui(5.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)
|
||||
minimum.y + ui(31.0f)
|
||||
};
|
||||
draw_toolbar_spinner(draw, center, ui(7.0f), text_color);
|
||||
draw_toolbar_spinner(draw, center, ui(8.5f), text_color);
|
||||
} else {
|
||||
draw->AddText(g_regular_font, icon_font_size, {icon_x, minimum.y + ui(19.0f)}, text_color, icon);
|
||||
draw->AddText(g_regular_font, icon_font_size, {icon_x, minimum.y + ui(21.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);
|
||||
draw->AddText({icon_x + icon_size.x + ui(7.0f), minimum.y + ui(24.0f)}, text_color, ICON_TB_CARET_DOWN);
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextUnformatted(tooltip);
|
||||
@@ -3030,7 +3109,7 @@ void draw_pull_options() {
|
||||
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 bool clicked = ImGui::InvisibleButton("##selector", {ui(logical_width), ui(54.0f)});
|
||||
const ImVec2 minimum = ImGui::GetItemRectMin();
|
||||
const ImVec2 maximum = ImGui::GetItemRectMax();
|
||||
ImDrawList* draw = ImGui::GetWindowDrawList();
|
||||
@@ -3038,7 +3117,7 @@ bool toolbar_selector(const char* id, const char* label, const std::string& valu
|
||||
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);
|
||||
{minimum.x + ui(8), minimum.y + ui(5.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) {
|
||||
@@ -3053,8 +3132,8 @@ bool toolbar_selector(const char* id, const char* label, const std::string& valu
|
||||
}
|
||||
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),
|
||||
draw->AddText({minimum.x + ui(8), minimum.y + ui(30.0f)}, IM_COL32(226, 229, 233, 255), displayed.c_str());
|
||||
draw->AddText({maximum.x - ui(18), minimum.y + ui(30.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());
|
||||
@@ -3759,7 +3838,9 @@ void draw_footer() {
|
||||
|
||||
void draw_new_tab() {
|
||||
ImGui::BeginChild("new_tab", {-1, -ui(28.0f)}, ImGuiChildFlags_None);
|
||||
const float margin = ui(84.0f);
|
||||
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);
|
||||
@@ -3767,45 +3848,25 @@ void draw_new_tab() {
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::PopFont();
|
||||
ImGui::SetCursorPosX(margin);
|
||||
if (ImGui::Button(ICON_TB_FOLDER_OPEN " Open", {ui(92.0f), ui(36.0f)}))
|
||||
if (ImGui::Button(ICON_TB_FOLDER_OPEN " Open", {ui(112.0f), ui(40.0f)}))
|
||||
pick_and_open_repository();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(ICON_TB_DOWNLOAD " Clone", {ui(96.0f), ui(36.0f)}))
|
||||
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();
|
||||
if (ImGui::Button(ICON_TB_PLUS " Create", {ui(100.0f), ui(36.0f)}))
|
||||
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(std::max(ui(280.0f), ImGui::GetContentRegionAvail().x - 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(7.0f)});
|
||||
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) {
|
||||
@@ -3813,10 +3874,12 @@ void draw_new_tab() {
|
||||
};
|
||||
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", {-margin, -ui(8.0f)}, ImGuiChildFlags_None);
|
||||
ImGui::BeginChild("repository_list", {content_width, -ui(8.0f)}, ImGuiChildFlags_None);
|
||||
if (repositories.empty() && !g_repository_filter[0]) {
|
||||
ImGui::TextDisabled("No recent repositories yet");
|
||||
}
|
||||
int visible_index = 0;
|
||||
for (const std::string& repository_path : repositories) {
|
||||
const std::filesystem::path path(repository_path);
|
||||
@@ -3828,16 +3891,17 @@ void draw_new_tab() {
|
||||
continue;
|
||||
ImGui::PushID(visible_index++);
|
||||
const bool clicked = ImGui::Selectable("##repository", false,
|
||||
ImGuiSelectableFlags_None, {0, ui(22.0f)});
|
||||
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(3.0f), minimum.y + ui(2.0f)},
|
||||
{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 + name_width + ui(14.0f), minimum.y + ui(2.0f)},
|
||||
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()) {
|
||||
@@ -3858,6 +3922,7 @@ void draw_new_tab() {
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
|
||||
void draw_app() {
|
||||
ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||
ImGui::SetNextWindowPos(viewport->WorkPos);
|
||||
@@ -4050,7 +4115,7 @@ void draw_app() {
|
||||
|
||||
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,
|
||||
ImGui::BeginChild("repo_toolbar", {-1, ui(58)}, ImGuiChildFlags_None,
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
ImGui::SetCursorPos({ui(5), ui(2)});
|
||||
ImGui::PushFont(g_bold_font, 0.0f);
|
||||
@@ -4114,7 +4179,7 @@ void draw_app() {
|
||||
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)});
|
||||
ImGui::SetCursorPos({std::max(selectors_right + ui(10.0f), centered_x), ui(1.0f)});
|
||||
|
||||
if (toolbar_action("undo", "Undo", ICON_TB_ROTATE_LEFT, repo().undo_action.tooltip.c_str(),
|
||||
repo().undo_action.available, false, 54))
|
||||
@@ -4229,10 +4294,12 @@ int runGitree(int argc, char** argv) {
|
||||
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();
|
||||
g_sidebar_width = user_data.sidebarWidth();
|
||||
g_details_width = user_data.detailsWidth();
|
||||
g_sidebar_collapsed = user_data.sidebarCollapsed();
|
||||
g_zoom_percent = user_data.zoomPercent();
|
||||
g_commit_message_height = user_data.commitMessageHeight();
|
||||
g_working_composer_height = user_data.workingComposerHeight();
|
||||
|
||||
if (argc > 1) {
|
||||
create_new_tab(false);
|
||||
@@ -4245,6 +4312,7 @@ int runGitree(int argc, char** argv) {
|
||||
}
|
||||
g_active_tab = std::min(user_data.activeRepository(), g_tabs.size() - 1);
|
||||
g_tab_to_select = g_tabs[g_active_tab].get();
|
||||
restore_pending_commit_draft(g_active_tab); // restore draft for active tab
|
||||
} else {
|
||||
create_new_tab(false);
|
||||
}
|
||||
@@ -4257,6 +4325,7 @@ int runGitree(int argc, char** argv) {
|
||||
ApplicationIconCache application_icon_cache;
|
||||
g_application_icon_cache = &application_icon_cache;
|
||||
|
||||
remove_stray_imgui_ini();
|
||||
IMGUI_CHECKVERSION();
|
||||
ImGui::CreateContext();
|
||||
ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||
@@ -4299,6 +4368,9 @@ int runGitree(int argc, char** argv) {
|
||||
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);
|
||||
user_data.setCommitMessageHeight(g_commit_message_height);
|
||||
user_data.setWorkingComposerHeight(g_working_composer_height);
|
||||
save_pending_commit_draft(); // persist any unsaved commit draft
|
||||
persist_repository_session();
|
||||
user_data.save();
|
||||
ImGui::DestroyContext();
|
||||
|
||||
@@ -74,16 +74,20 @@ void drawOrthogonalEdge(ImDrawList* draw, const ImVec2& child, const ImVec2& par
|
||||
return;
|
||||
}
|
||||
|
||||
// Offset detour route lines horizontally by slot to avoid lane collisions
|
||||
const float slot_offset = static_cast<float>(route_slot) * 4.0f * scale;
|
||||
|
||||
if (std::isfinite(detour_x)) {
|
||||
if (std::abs(detour_x - child.x) < 0.5f * scale) {
|
||||
drawRoundedPolyline(draw, std::array{child, ImVec2{detour_x, parent.y}, parent},
|
||||
const float final_detour_x = detour_x + slot_offset;
|
||||
if (std::abs(final_detour_x - child.x) < 0.5f * scale) {
|
||||
drawRoundedPolyline(draw, std::array{child, ImVec2{final_detour_x, parent.y}, parent},
|
||||
7.0f * scale, color, thickness);
|
||||
} else if (std::abs(detour_x - parent.x) < 0.5f * scale) {
|
||||
drawRoundedPolyline(draw, std::array{child, ImVec2{detour_x, child.y}, parent},
|
||||
} else if (std::abs(final_detour_x - parent.x) < 0.5f * scale) {
|
||||
drawRoundedPolyline(draw, std::array{child, ImVec2{final_detour_x, child.y}, parent},
|
||||
7.0f * scale, color, thickness);
|
||||
} else {
|
||||
drawRoundedPolyline(draw,
|
||||
std::array{child, ImVec2{detour_x, child.y}, ImVec2{detour_x, parent.y}, parent},
|
||||
std::array{child, ImVec2{final_detour_x, child.y}, ImVec2{final_detour_x, parent.y}, parent},
|
||||
7.0f * scale, color, thickness);
|
||||
}
|
||||
return;
|
||||
@@ -124,7 +128,7 @@ float GraphRenderer::requiredWidth(const std::vector<CommitInfo>& commits, float
|
||||
int maximum_lane = 0;
|
||||
for (const auto& commit : commits) maximum_lane = std::max(maximum_lane, commit.lane);
|
||||
// Keep one spare lane available for edges that must route around occupied commit lanes.
|
||||
return std::max((68.0f + maximum_lane * 22.0f) * scale, 82.0f * scale);
|
||||
return std::max((68.0f + maximum_lane * 28.0f) * scale, 82.0f * scale);
|
||||
}
|
||||
|
||||
void GraphRenderer::drawRow(int row, const CommitInfo& commit,
|
||||
@@ -135,7 +139,7 @@ void GraphRenderer::drawRow(int row, const CommitInfo& commit,
|
||||
const ImVec2 origin = ImGui::GetCursorScreenPos();
|
||||
const float row_height = row_heights[static_cast<size_t>(row)];
|
||||
const float content_height = std::max(px(1.0f), row_height - ImGui::GetStyle().CellPadding.y * 2.0f);
|
||||
const float lane_spacing = px(22.0f);
|
||||
const float lane_spacing = px(28.0f);
|
||||
const float x = origin.x + px(17.0f) + lane_spacing * commit.lane;
|
||||
const float y = origin.y + content_height * 0.5f;
|
||||
const float cell_right = origin.x + ImGui::GetContentRegionAvail().x;
|
||||
|
||||
1
vendor/iKv
vendored
1
vendor/iKv
vendored
Submodule vendor/iKv deleted from d0b02f4735
1
vendor/iKvxx
vendored
Submodule
1
vendor/iKvxx
vendored
Submodule
Submodule vendor/iKvxx added at 99d3ea7b83
Reference in New Issue
Block a user