Merge branch 'main' into prod
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
name: Build Windows EXE
|
||||
name: Build Releases
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
|
||||
env:
|
||||
APP_NAME: gitree
|
||||
|
||||
jobs:
|
||||
build-windows:
|
||||
name: Build Windows x64
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -16,6 +20,16 @@ jobs:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate build names
|
||||
env:
|
||||
GITEA_SHA: ${{ gitea.sha }}
|
||||
run: |
|
||||
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"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -26,10 +40,13 @@ jobs:
|
||||
cat > mingw-toolchain.cmake <<'EOF'
|
||||
set(CMAKE_SYSTEM_NAME Windows)
|
||||
set(CMAKE_SYSTEM_PROCESSOR x86_64)
|
||||
|
||||
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
|
||||
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
|
||||
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
|
||||
|
||||
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
|
||||
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
@@ -46,9 +63,32 @@ jobs:
|
||||
- name: Build Windows executable
|
||||
run: cmake --build build-win --parallel
|
||||
|
||||
- name: Locate Windows EXE
|
||||
run: |
|
||||
exe_path=""
|
||||
|
||||
if [ -f "build-win/bin/${APP_NAME}.exe" ]; then
|
||||
exe_path="build-win/bin/${APP_NAME}.exe"
|
||||
elif [ -f "build-win/bin/Gitree.exe" ]; then
|
||||
exe_path="build-win/bin/Gitree.exe"
|
||||
elif [ -f "build-win/bin/gitree.exe" ]; then
|
||||
exe_path="build-win/bin/gitree.exe"
|
||||
else
|
||||
exe_path="$(find build-win -type f -iname '*.exe' | head -n 1)"
|
||||
fi
|
||||
|
||||
if [ -z "$exe_path" ]; then
|
||||
echo "Could not find built Windows EXE."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "WINDOWS_EXE_PATH=${exe_path}" >> "$GITHUB_ENV"
|
||||
echo "Found Windows EXE: ${exe_path}"
|
||||
|
||||
- name: Verify static runtime linkage
|
||||
run: |
|
||||
dependencies="$(x86_64-w64-mingw32-objdump -p build-win/bin/gitree.exe | sed -n 's/.*DLL Name: //p')"
|
||||
dependencies="$(x86_64-w64-mingw32-objdump -p "$WINDOWS_EXE_PATH" | sed -n 's/.*DLL Name: //p')"
|
||||
|
||||
printf '%s\n' "$dependencies"
|
||||
|
||||
if printf '%s\n' "$dependencies" | grep -Eqi 'lib(gcc|stdc\+\+|winpthread).*\.dll'; then
|
||||
@@ -56,36 +96,174 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Generate EXE name
|
||||
run: |
|
||||
repo_name="$(basename "${GITEA_REPOSITORY}")"
|
||||
short_sha="$(printf '%s' "${GITEA_SHA}" | cut -c1-7)"
|
||||
|
||||
exe_name="${repo_name}-windows-x64-${GITEA_RUN_NUMBER}-${short_sha}.exe"
|
||||
|
||||
echo "EXE_NAME=${exe_name}" >> "$GITHUB_ENV"
|
||||
echo "ARTIFACT_NAME=${repo_name}-windows-x64-${GITEA_RUN_NUMBER}-${short_sha}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Prepare artifact
|
||||
- name: Prepare Windows artifact
|
||||
run: |
|
||||
mkdir -p dist
|
||||
cp build-win/bin/gitree.exe "dist/${EXE_NAME}"
|
||||
cp "$WINDOWS_EXE_PATH" "dist/${WINDOWS_EXE}"
|
||||
|
||||
- name: Upload Windows build
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
path: dist/${{ env.EXE_NAME }}
|
||||
name: ${{ env.WINDOWS_ARTIFACT }}
|
||||
path: dist/${{ env.WINDOWS_EXE }}
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Create prod release
|
||||
if: ${{ gitea.ref_name == 'prod' }}
|
||||
build-linux:
|
||||
name: Build Linux x64 DEB
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate build names
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
GITEA_SHA: ${{ gitea.sha }}
|
||||
GITEA_RUN_NUMBER: ${{ gitea.run_number }}
|
||||
run: |
|
||||
short_sha="$(printf '%s' "$GITEA_SHA" | cut -c1-7)"
|
||||
|
||||
echo "BUILD_HASH=${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"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y cmake ninja-build curl jq dpkg-dev
|
||||
|
||||
- name: Configure Linux release build
|
||||
run: |
|
||||
cmake -S . -B build-linux -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_SHARED_LIBS=OFF
|
||||
|
||||
- name: Build Linux executable
|
||||
run: cmake --build build-linux --parallel
|
||||
|
||||
- name: Locate Linux executable
|
||||
run: |
|
||||
exe_path=""
|
||||
|
||||
if [ -f "build-linux/bin/${APP_NAME}" ]; then
|
||||
exe_path="build-linux/bin/${APP_NAME}"
|
||||
elif [ -f "build-linux/bin/Gitree" ]; then
|
||||
exe_path="build-linux/bin/Gitree"
|
||||
elif [ -f "build-linux/bin/gitree" ]; then
|
||||
exe_path="build-linux/bin/gitree"
|
||||
else
|
||||
exe_path="$(find build-linux -type f -executable -name "${APP_NAME}" | head -n 1)"
|
||||
fi
|
||||
|
||||
if [ -z "$exe_path" ]; then
|
||||
echo "Could not find built Linux executable."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "LINUX_EXE_PATH=${exe_path}" >> "$GITHUB_ENV"
|
||||
echo "Found Linux executable: ${exe_path}"
|
||||
|
||||
- name: Package Linux DEB
|
||||
run: |
|
||||
mkdir -p "pkg/DEBIAN"
|
||||
mkdir -p "pkg/usr/bin"
|
||||
|
||||
cp "$LINUX_EXE_PATH" "pkg/usr/bin/${APP_NAME}"
|
||||
chmod 755 "pkg/usr/bin/${APP_NAME}"
|
||||
|
||||
version="0.0.0+${BUILD_HASH}"
|
||||
|
||||
cat > "pkg/DEBIAN/control" <<EOF
|
||||
Package: ${APP_NAME}
|
||||
Version: ${version}
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Architecture: amd64
|
||||
Maintainer: GigabiteStudios <ceo@gigabitestudios.xyz>
|
||||
Description: ${APP_NAME}
|
||||
${APP_NAME} Linux x64 build.
|
||||
EOF
|
||||
|
||||
mkdir -p dist
|
||||
dpkg-deb --build pkg "dist/${LINUX_DEB}"
|
||||
|
||||
- name: Upload Linux DEB build
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ env.LINUX_ARTIFACT }}
|
||||
path: dist/${{ env.LINUX_DEB }}
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-windows
|
||||
- build-linux
|
||||
if: ${{ always() && gitea.ref_name == 'prod' }}
|
||||
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
GITEA_SHA: ${{ gitea.sha }}
|
||||
WINDOWS_RESULT: ${{ needs.build-windows.result }}
|
||||
LINUX_RESULT: ${{ needs.build-linux.result }}
|
||||
APP_NAME: gitree
|
||||
|
||||
steps:
|
||||
- name: Check build results
|
||||
run: |
|
||||
echo "Windows result: ${WINDOWS_RESULT}"
|
||||
echo "Linux result: ${LINUX_RESULT}"
|
||||
|
||||
if [ "$WINDOWS_RESULT" != "success" ] && [ "$LINUX_RESULT" != "success" ]; then
|
||||
echo "Both Windows and Linux builds failed. Refusing to create release."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate release names
|
||||
run: |
|
||||
short_sha="$(printf '%s' "$GITEA_SHA" | cut -c1-7)"
|
||||
|
||||
echo "BUILD_HASH=${short_sha}" >> "$GITHUB_ENV"
|
||||
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 "LINUX_DEB=${APP_NAME}-deb-x64-${short_sha}.deb" >> "$GITHUB_ENV"
|
||||
echo "LINUX_ARTIFACT=${APP_NAME}-deb-x64-${short_sha}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install release dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y curl jq
|
||||
|
||||
- name: Download Windows artifact
|
||||
if: ${{ needs.build-windows.result == 'success' }}
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ env.WINDOWS_ARTIFACT }}
|
||||
path: release-assets
|
||||
|
||||
- name: Download Linux artifact
|
||||
if: ${{ needs.build-linux.result == 'success' }}
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ env.LINUX_ARTIFACT }}
|
||||
path: release-assets
|
||||
|
||||
- name: Create prod release
|
||||
run: |
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
echo "The repository secret GITEA_TOKEN is required to publish prod releases."
|
||||
@@ -94,8 +272,6 @@ jobs:
|
||||
|
||||
git fetch --tags --force
|
||||
|
||||
short_sha="$(printf '%s' "$GITEA_SHA" | cut -c1-7)"
|
||||
tag="prod-${GITEA_RUN_NUMBER}-${short_sha}"
|
||||
api="${GITEA_SERVER_URL%/}/api/v1/repos/${GITEA_REPOSITORY}"
|
||||
repo_url="${GITEA_SERVER_URL%/}/${GITEA_REPOSITORY}"
|
||||
|
||||
@@ -103,21 +279,16 @@ jobs:
|
||||
curl --fail-with-body --silent --show-error \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${api}/releases?limit=50" |
|
||||
jq -r '[.[] | select(.tag_name | startswith("prod-"))][0].tag_name // empty'
|
||||
jq -r '[.[] | select(.tag_name | startswith("release-"))][0].tag_name // empty'
|
||||
)"
|
||||
|
||||
if [ -n "$previous_tag" ]; then
|
||||
range="${previous_tag}..${GITEA_SHA}"
|
||||
echo "Generating release notes from ${previous_tag} to ${GITEA_SHA}"
|
||||
else
|
||||
range="${GITEA_SHA}"
|
||||
echo "No previous prod release found. Generating release notes from current commit."
|
||||
fi
|
||||
|
||||
{
|
||||
echo "Automated Windows release from the prod branch."
|
||||
echo
|
||||
|
||||
if [ -n "$previous_tag" ]; then
|
||||
echo "## Changes since ${previous_tag}"
|
||||
else
|
||||
@@ -140,6 +311,22 @@ jobs:
|
||||
done
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "## Builds"
|
||||
echo
|
||||
|
||||
if [ "$WINDOWS_RESULT" = "success" ]; then
|
||||
echo "- ${WINDOWS_EXE}"
|
||||
else
|
||||
echo "- Windows x64 failed"
|
||||
fi
|
||||
|
||||
if [ "$LINUX_RESULT" = "success" ]; then
|
||||
echo "- ${LINUX_DEB}"
|
||||
else
|
||||
echo "- Linux DEB x64 failed"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "## Authors"
|
||||
echo
|
||||
@@ -175,9 +362,9 @@ jobs:
|
||||
} > release-notes.md
|
||||
|
||||
payload="$(jq -n \
|
||||
--arg tag "$tag" \
|
||||
--arg tag "$RELEASE_TAG" \
|
||||
--arg sha "$GITEA_SHA" \
|
||||
--arg name "${ARTIFACT_NAME}" \
|
||||
--arg name "$RELEASE_NAME" \
|
||||
--rawfile body release-notes.md \
|
||||
'{
|
||||
tag_name: $tag,
|
||||
@@ -197,8 +384,18 @@ jobs:
|
||||
|
||||
release_id="$(printf '%s' "$release" | jq -er '.id')"
|
||||
|
||||
curl --fail-with-body --silent --show-error \
|
||||
-X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-F "attachment=@dist/${EXE_NAME}" \
|
||||
"${api}/releases/${release_id}/assets?name=${EXE_NAME}"
|
||||
for asset in release-assets/*; do
|
||||
if [ ! -f "$asset" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
asset_name="$(basename "$asset")"
|
||||
|
||||
echo "Uploading ${asset_name}"
|
||||
|
||||
curl --fail-with-body --silent --show-error \
|
||||
-X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-F "attachment=@${asset}" \
|
||||
"${api}/releases/${release_id}/assets?name=${asset_name}"
|
||||
done
|
||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -1,12 +1,12 @@
|
||||
[submodule "vendor/libgit2"]
|
||||
path = vendor/libgit2
|
||||
url = https://github.com/libgit2/libgit2.git
|
||||
url = https://dock-it.dev/Idea-Studios/libgit2
|
||||
[submodule "vendor/imgui"]
|
||||
path = vendor/imgui
|
||||
url = https://github.com/ocornut/imgui.git
|
||||
url = https://dock-it.dev/Idea-Studios/imgui
|
||||
[submodule "vendor/glfw"]
|
||||
path = vendor/glfw
|
||||
url = https://github.com/glfw/glfw.git
|
||||
url = https://dock-it.dev/Idea-Studios/glfw
|
||||
[submodule "vendor/iZo"]
|
||||
path = vendor/iZo
|
||||
url = https://dock-it.dev/Idea-Studios/iZo
|
||||
|
||||
@@ -14,20 +14,31 @@ namespace
|
||||
return value && !value->empty() ? std::filesystem::path(*value) : std::filesystem::path{};
|
||||
}
|
||||
|
||||
std::filesystem::path under(const std::filesystem::path &root, const char *relative)
|
||||
{
|
||||
return root.empty() ? std::filesystem::path{} : root / relative;
|
||||
}
|
||||
|
||||
std::filesystem::path firstExisting(std::initializer_list<std::filesystem::path> candidates)
|
||||
{
|
||||
std::error_code error;
|
||||
|
||||
for (const auto &candidate : candidates)
|
||||
{
|
||||
if (!candidate.empty() && std::filesystem::is_regular_file(candidate, error))
|
||||
return candidate;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::filesystem::path findExecutable(const std::filesystem::path &root, const char *filename)
|
||||
{
|
||||
std::error_code error;
|
||||
|
||||
if (root.empty() || !std::filesystem::is_directory(root, error))
|
||||
return {};
|
||||
|
||||
for (std::filesystem::recursive_directory_iterator iterator(
|
||||
root, std::filesystem::directory_options::skip_permission_denied, error),
|
||||
end;
|
||||
@@ -36,6 +47,7 @@ namespace
|
||||
if (iterator->is_regular_file(error) && iterator->path().filename() == filename)
|
||||
return iterator->path();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -43,9 +55,14 @@ namespace
|
||||
ApplicationManager::ApplicationManager()
|
||||
{
|
||||
const auto local = izo::GetKnownPath(izo::KnownPath::LocalAppData);
|
||||
const auto roaming = izo::GetKnownPath(izo::KnownPath::AppData);
|
||||
|
||||
const auto program_files = environmentPath("ProgramFiles");
|
||||
const auto program_files_x86 = environmentPath("ProgramFiles(x86)");
|
||||
const auto program_data = environmentPath("ProgramData");
|
||||
const auto user_profile = environmentPath("USERPROFILE");
|
||||
const auto windows = environmentPath("WINDIR");
|
||||
const auto chocolatey = environmentPath("ChocolateyInstall");
|
||||
|
||||
auto add = [this](ExternalApplicationId id, const char *name, std::filesystem::path executable,
|
||||
std::vector<std::string> arguments = {})
|
||||
@@ -55,99 +72,481 @@ ApplicationManager::ApplicationManager()
|
||||
applications_.push_back(targets_.back().info);
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// VS Code-style editors
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
add(ExternalApplicationId::visual_studio_code, "VS Code", firstExisting({
|
||||
local / "Programs/Microsoft VS Code/Code.exe",
|
||||
program_files / "Microsoft VS Code/Code.exe",
|
||||
program_files_x86 / "Microsoft VS Code/Code.exe",
|
||||
}));
|
||||
add(ExternalApplicationId::visual_studio, "Visual Studio", firstExisting({
|
||||
program_files / "Microsoft Visual Studio/2022/Community/Common7/IDE/devenv.exe",
|
||||
program_files / "Microsoft Visual Studio/2022/Professional/Common7/IDE/devenv.exe",
|
||||
program_files / "Microsoft Visual Studio/2022/Enterprise/Common7/IDE/devenv.exe",
|
||||
program_files_x86 / "Microsoft Visual Studio/2019/Community/Common7/IDE/devenv.exe",
|
||||
}));
|
||||
under(local, "Programs/Microsoft VS Code/Code.exe"),
|
||||
under(program_files, "Microsoft VS Code/Code.exe"),
|
||||
under(program_files_x86, "Microsoft VS Code/Code.exe"),
|
||||
under(user_profile, "scoop/apps/vscode/current/Code.exe"),
|
||||
under(chocolatey, "lib/vscode/tools/Code.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::visual_studio_code_insiders, "VS Code Insiders", firstExisting({
|
||||
under(local, "Programs/Microsoft VS Code Insiders/Code - Insiders.exe"),
|
||||
under(program_files, "Microsoft VS Code Insiders/Code - Insiders.exe"),
|
||||
under(user_profile, "scoop/apps/vscode-insiders/current/Code - Insiders.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::vscodium, "VSCodium", firstExisting({
|
||||
under(local, "Programs/VSCodium/VSCodium.exe"),
|
||||
under(program_files, "VSCodium/VSCodium.exe"),
|
||||
under(program_files_x86, "VSCodium/VSCodium.exe"),
|
||||
under(user_profile, "scoop/apps/vscodium/current/VSCodium.exe"),
|
||||
under(chocolatey, "lib/vscodium/tools/VSCodium.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::cursor, "Cursor", firstExisting({
|
||||
under(local, "Programs/cursor/Cursor.exe"),
|
||||
under(local, "Programs/Cursor/Cursor.exe"),
|
||||
under(program_files, "Cursor/Cursor.exe"),
|
||||
under(user_profile, "scoop/apps/cursor/current/Cursor.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::windsurf, "Windsurf", firstExisting({
|
||||
under(local, "Programs/Windsurf/Windsurf.exe"),
|
||||
under(program_files, "Windsurf/Windsurf.exe"),
|
||||
under(user_profile, "scoop/apps/windsurf/current/Windsurf.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::trae, "Trae", firstExisting({
|
||||
under(local, "Programs/Trae/Trae.exe"),
|
||||
under(program_files, "Trae/Trae.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::zed, "Zed", firstExisting({
|
||||
under(local, "Programs/Zed/Zed.exe"),
|
||||
under(program_files, "Zed/Zed.exe"),
|
||||
under(user_profile, "scoop/apps/zed/current/Zed.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::antigravity, "Antigravity", firstExisting({
|
||||
local / "Programs/Antigravity/Antigravity.exe",
|
||||
local / "Antigravity/Antigravity.exe",
|
||||
program_files / "Antigravity/Antigravity.exe",
|
||||
}));
|
||||
under(local, "Programs/Antigravity/Antigravity.exe"),
|
||||
under(local, "Antigravity/Antigravity.exe"),
|
||||
under(program_files, "Antigravity/Antigravity.exe"),
|
||||
}));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Microsoft IDEs
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
add(ExternalApplicationId::visual_studio, "Visual Studio", firstExisting({
|
||||
under(program_files, "Microsoft Visual Studio/2022/Community/Common7/IDE/devenv.exe"),
|
||||
under(program_files, "Microsoft Visual Studio/2022/Professional/Common7/IDE/devenv.exe"),
|
||||
under(program_files, "Microsoft Visual Studio/2022/Enterprise/Common7/IDE/devenv.exe"),
|
||||
under(program_files, "Microsoft Visual Studio/2022/Preview/Common7/IDE/devenv.exe"),
|
||||
|
||||
under(program_files_x86, "Microsoft Visual Studio/2019/Community/Common7/IDE/devenv.exe"),
|
||||
under(program_files_x86, "Microsoft Visual Studio/2019/Professional/Common7/IDE/devenv.exe"),
|
||||
under(program_files_x86, "Microsoft Visual Studio/2019/Enterprise/Common7/IDE/devenv.exe"),
|
||||
under(program_files_x86, "Microsoft Visual Studio/2019/Preview/Common7/IDE/devenv.exe"),
|
||||
}));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// GitHub / Windows tools
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
std::filesystem::path github_desktop = firstExisting({
|
||||
local / "GitHubDesktop/GitHubDesktop.exe",
|
||||
local / "GitHub Desktop/GitHubDesktop.exe",
|
||||
under(local, "GitHubDesktop/GitHubDesktop.exe"),
|
||||
under(local, "GitHub Desktop/GitHubDesktop.exe"),
|
||||
});
|
||||
|
||||
if (github_desktop.empty())
|
||||
github_desktop = findExecutable(local / "GitHubDesktop", "GitHubDesktop.exe");
|
||||
github_desktop = findExecutable(under(local, "GitHubDesktop"), "GitHubDesktop.exe");
|
||||
|
||||
add(ExternalApplicationId::github_desktop, "GitHub Desktop", std::move(github_desktop));
|
||||
add(ExternalApplicationId::file_explorer, "File Explorer",
|
||||
firstExisting({windows / "explorer.exe"}));
|
||||
add(ExternalApplicationId::terminal, "Terminal",
|
||||
firstExisting({local / "Microsoft/WindowsApps/wt.exe", windows / "System32/wt.exe"}), {"-d"});
|
||||
|
||||
add(ExternalApplicationId::file_explorer, "File Explorer", firstExisting({
|
||||
under(windows, "explorer.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::terminal, "Terminal", firstExisting({
|
||||
under(local, "Microsoft/WindowsApps/wt.exe"),
|
||||
under(windows, "System32/wt.exe"),
|
||||
}), {"-d"});
|
||||
|
||||
add(ExternalApplicationId::git_bash, "Git Bash", firstExisting({
|
||||
program_files / "Git/git-bash.exe",
|
||||
program_files_x86 / "Git/git-bash.exe",
|
||||
}),
|
||||
{"--cd="});
|
||||
add(ExternalApplicationId::wsl, "WSL", firstExisting({windows / "System32/wsl.exe"}), {"--cd"});
|
||||
under(program_files, "Git/git-bash.exe"),
|
||||
under(program_files_x86, "Git/git-bash.exe"),
|
||||
under(user_profile, "scoop/apps/git/current/git-bash.exe"),
|
||||
}), {"--cd="});
|
||||
|
||||
add(ExternalApplicationId::wsl, "WSL", firstExisting({
|
||||
under(windows, "System32/wsl.exe"),
|
||||
}), {"--cd"});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Android
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
add(ExternalApplicationId::android_studio, "Android Studio", firstExisting({
|
||||
program_files / "Android/Android Studio/bin/studio64.exe",
|
||||
local / "Programs/Android Studio/bin/studio64.exe",
|
||||
}));
|
||||
under(program_files, "Android/Android Studio/bin/studio64.exe"),
|
||||
under(local, "Programs/Android Studio/bin/studio64.exe"),
|
||||
under(user_profile, "scoop/apps/android-studio/current/bin/studio64.exe"),
|
||||
}));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// JetBrains IDEs
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
std::filesystem::path idea = firstExisting({
|
||||
program_files / "JetBrains/IntelliJ IDEA 2025.1/bin/idea64.exe",
|
||||
program_files / "JetBrains/IntelliJ IDEA 2024.3/bin/idea64.exe",
|
||||
local / "Programs/IntelliJ IDEA Ultimate/bin/idea64.exe",
|
||||
under(program_files, "JetBrains/IntelliJ IDEA 2026.1/bin/idea64.exe"),
|
||||
under(program_files, "JetBrains/IntelliJ IDEA 2025.3/bin/idea64.exe"),
|
||||
under(program_files, "JetBrains/IntelliJ IDEA 2025.2/bin/idea64.exe"),
|
||||
under(program_files, "JetBrains/IntelliJ IDEA 2025.1/bin/idea64.exe"),
|
||||
under(program_files, "JetBrains/IntelliJ IDEA 2024.3/bin/idea64.exe"),
|
||||
|
||||
under(program_files, "JetBrains/IntelliJ IDEA Community Edition 2026.1/bin/idea64.exe"),
|
||||
under(program_files, "JetBrains/IntelliJ IDEA Community Edition 2025.3/bin/idea64.exe"),
|
||||
under(program_files, "JetBrains/IntelliJ IDEA Community Edition 2025.2/bin/idea64.exe"),
|
||||
under(program_files, "JetBrains/IntelliJ IDEA Community Edition 2025.1/bin/idea64.exe"),
|
||||
under(program_files, "JetBrains/IntelliJ IDEA Community Edition 2024.3/bin/idea64.exe"),
|
||||
|
||||
under(local, "Programs/IntelliJ IDEA Ultimate/bin/idea64.exe"),
|
||||
under(local, "Programs/IntelliJ IDEA Community/bin/idea64.exe"),
|
||||
});
|
||||
|
||||
if (idea.empty())
|
||||
idea = findExecutable(local / "JetBrains/Toolbox/apps", "idea64.exe");
|
||||
idea = findExecutable(under(local, "JetBrains/Toolbox/apps"), "idea64.exe");
|
||||
|
||||
add(ExternalApplicationId::intellij_idea, "IntelliJ IDEA", std::move(idea));
|
||||
|
||||
std::filesystem::path clion = firstExisting({
|
||||
under(program_files, "JetBrains/CLion 2026.1/bin/clion64.exe"),
|
||||
under(program_files, "JetBrains/CLion 2025.3/bin/clion64.exe"),
|
||||
under(program_files, "JetBrains/CLion 2025.2/bin/clion64.exe"),
|
||||
under(program_files, "JetBrains/CLion 2025.1/bin/clion64.exe"),
|
||||
under(program_files, "JetBrains/CLion 2024.3/bin/clion64.exe"),
|
||||
under(local, "Programs/CLion/bin/clion64.exe"),
|
||||
});
|
||||
|
||||
if (clion.empty())
|
||||
clion = findExecutable(under(local, "JetBrains/Toolbox/apps"), "clion64.exe");
|
||||
|
||||
add(ExternalApplicationId::clion, "CLion", std::move(clion));
|
||||
|
||||
std::filesystem::path rider = firstExisting({
|
||||
under(program_files, "JetBrains/JetBrains Rider 2026.1/bin/rider64.exe"),
|
||||
under(program_files, "JetBrains/JetBrains Rider 2025.3/bin/rider64.exe"),
|
||||
under(program_files, "JetBrains/JetBrains Rider 2025.2/bin/rider64.exe"),
|
||||
under(program_files, "JetBrains/JetBrains Rider 2025.1/bin/rider64.exe"),
|
||||
under(program_files, "JetBrains/JetBrains Rider 2024.3/bin/rider64.exe"),
|
||||
under(local, "Programs/Rider/bin/rider64.exe"),
|
||||
});
|
||||
|
||||
if (rider.empty())
|
||||
rider = findExecutable(under(local, "JetBrains/Toolbox/apps"), "rider64.exe");
|
||||
|
||||
add(ExternalApplicationId::rider, "Rider", std::move(rider));
|
||||
|
||||
std::filesystem::path pycharm = firstExisting({
|
||||
under(program_files, "JetBrains/PyCharm 2026.1/bin/pycharm64.exe"),
|
||||
under(program_files, "JetBrains/PyCharm 2025.3/bin/pycharm64.exe"),
|
||||
under(program_files, "JetBrains/PyCharm 2025.2/bin/pycharm64.exe"),
|
||||
under(program_files, "JetBrains/PyCharm 2025.1/bin/pycharm64.exe"),
|
||||
under(program_files, "JetBrains/PyCharm 2024.3/bin/pycharm64.exe"),
|
||||
|
||||
under(program_files, "JetBrains/PyCharm Community Edition 2026.1/bin/pycharm64.exe"),
|
||||
under(program_files, "JetBrains/PyCharm Community Edition 2025.3/bin/pycharm64.exe"),
|
||||
under(program_files, "JetBrains/PyCharm Community Edition 2025.2/bin/pycharm64.exe"),
|
||||
under(program_files, "JetBrains/PyCharm Community Edition 2025.1/bin/pycharm64.exe"),
|
||||
under(program_files, "JetBrains/PyCharm Community Edition 2024.3/bin/pycharm64.exe"),
|
||||
|
||||
under(local, "Programs/PyCharm Professional/bin/pycharm64.exe"),
|
||||
under(local, "Programs/PyCharm Community/bin/pycharm64.exe"),
|
||||
});
|
||||
|
||||
if (pycharm.empty())
|
||||
pycharm = findExecutable(under(local, "JetBrains/Toolbox/apps"), "pycharm64.exe");
|
||||
|
||||
add(ExternalApplicationId::pycharm, "PyCharm", std::move(pycharm));
|
||||
|
||||
std::filesystem::path webstorm = firstExisting({
|
||||
under(program_files, "JetBrains/WebStorm 2026.1/bin/webstorm64.exe"),
|
||||
under(program_files, "JetBrains/WebStorm 2025.3/bin/webstorm64.exe"),
|
||||
under(program_files, "JetBrains/WebStorm 2025.2/bin/webstorm64.exe"),
|
||||
under(program_files, "JetBrains/WebStorm 2025.1/bin/webstorm64.exe"),
|
||||
under(program_files, "JetBrains/WebStorm 2024.3/bin/webstorm64.exe"),
|
||||
under(local, "Programs/WebStorm/bin/webstorm64.exe"),
|
||||
});
|
||||
|
||||
if (webstorm.empty())
|
||||
webstorm = findExecutable(under(local, "JetBrains/Toolbox/apps"), "webstorm64.exe");
|
||||
|
||||
add(ExternalApplicationId::webstorm, "WebStorm", std::move(webstorm));
|
||||
|
||||
std::filesystem::path phpstorm = firstExisting({
|
||||
under(program_files, "JetBrains/PhpStorm 2026.1/bin/phpstorm64.exe"),
|
||||
under(program_files, "JetBrains/PhpStorm 2025.3/bin/phpstorm64.exe"),
|
||||
under(program_files, "JetBrains/PhpStorm 2025.2/bin/phpstorm64.exe"),
|
||||
under(program_files, "JetBrains/PhpStorm 2025.1/bin/phpstorm64.exe"),
|
||||
under(program_files, "JetBrains/PhpStorm 2024.3/bin/phpstorm64.exe"),
|
||||
under(local, "Programs/PhpStorm/bin/phpstorm64.exe"),
|
||||
});
|
||||
|
||||
if (phpstorm.empty())
|
||||
phpstorm = findExecutable(under(local, "JetBrains/Toolbox/apps"), "phpstorm64.exe");
|
||||
|
||||
add(ExternalApplicationId::phpstorm, "PhpStorm", std::move(phpstorm));
|
||||
|
||||
std::filesystem::path rubymine = firstExisting({
|
||||
under(program_files, "JetBrains/RubyMine 2026.1/bin/rubymine64.exe"),
|
||||
under(program_files, "JetBrains/RubyMine 2025.3/bin/rubymine64.exe"),
|
||||
under(program_files, "JetBrains/RubyMine 2025.2/bin/rubymine64.exe"),
|
||||
under(program_files, "JetBrains/RubyMine 2025.1/bin/rubymine64.exe"),
|
||||
under(program_files, "JetBrains/RubyMine 2024.3/bin/rubymine64.exe"),
|
||||
under(local, "Programs/RubyMine/bin/rubymine64.exe"),
|
||||
});
|
||||
|
||||
if (rubymine.empty())
|
||||
rubymine = findExecutable(under(local, "JetBrains/Toolbox/apps"), "rubymine64.exe");
|
||||
|
||||
add(ExternalApplicationId::rubymine, "RubyMine", std::move(rubymine));
|
||||
|
||||
std::filesystem::path goland = firstExisting({
|
||||
under(program_files, "JetBrains/GoLand 2026.1/bin/goland64.exe"),
|
||||
under(program_files, "JetBrains/GoLand 2025.3/bin/goland64.exe"),
|
||||
under(program_files, "JetBrains/GoLand 2025.2/bin/goland64.exe"),
|
||||
under(program_files, "JetBrains/GoLand 2025.1/bin/goland64.exe"),
|
||||
under(program_files, "JetBrains/GoLand 2024.3/bin/goland64.exe"),
|
||||
under(local, "Programs/GoLand/bin/goland64.exe"),
|
||||
});
|
||||
|
||||
if (goland.empty())
|
||||
goland = findExecutable(under(local, "JetBrains/Toolbox/apps"), "goland64.exe");
|
||||
|
||||
add(ExternalApplicationId::goland, "GoLand", std::move(goland));
|
||||
|
||||
std::filesystem::path datagrip = firstExisting({
|
||||
under(program_files, "JetBrains/DataGrip 2026.1/bin/datagrip64.exe"),
|
||||
under(program_files, "JetBrains/DataGrip 2025.3/bin/datagrip64.exe"),
|
||||
under(program_files, "JetBrains/DataGrip 2025.2/bin/datagrip64.exe"),
|
||||
under(program_files, "JetBrains/DataGrip 2025.1/bin/datagrip64.exe"),
|
||||
under(program_files, "JetBrains/DataGrip 2024.3/bin/datagrip64.exe"),
|
||||
under(local, "Programs/DataGrip/bin/datagrip64.exe"),
|
||||
});
|
||||
|
||||
if (datagrip.empty())
|
||||
datagrip = findExecutable(under(local, "JetBrains/Toolbox/apps"), "datagrip64.exe");
|
||||
|
||||
add(ExternalApplicationId::datagrip, "DataGrip", std::move(datagrip));
|
||||
|
||||
add(ExternalApplicationId::jetbrains_fleet, "JetBrains Fleet", firstExisting({
|
||||
under(local, "Programs/Fleet/Fleet.exe"),
|
||||
under(local, "JetBrains/Fleet/bin/Fleet.exe"),
|
||||
under(program_files, "JetBrains/Fleet/Fleet.exe"),
|
||||
}));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Lightweight / general editors
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
add(ExternalApplicationId::sublime_text, "Sublime Text", firstExisting({
|
||||
under(program_files, "Sublime Text/sublime_text.exe"),
|
||||
under(program_files, "Sublime Text 3/sublime_text.exe"),
|
||||
under(program_files_x86, "Sublime Text/sublime_text.exe"),
|
||||
under(local, "Programs/Sublime Text/sublime_text.exe"),
|
||||
under(user_profile, "scoop/apps/sublime-text/current/sublime_text.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::notepad_plus_plus, "Notepad++", firstExisting({
|
||||
under(program_files, "Notepad++/notepad++.exe"),
|
||||
under(program_files_x86, "Notepad++/notepad++.exe"),
|
||||
under(user_profile, "scoop/apps/notepadplusplus/current/notepad++.exe"),
|
||||
under(chocolatey, "lib/notepadplusplus/tools/notepad++.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::atom, "Atom", firstExisting({
|
||||
under(local, "atom/atom.exe"),
|
||||
under(local, "Programs/Atom/atom.exe"),
|
||||
under(program_files, "Atom/atom.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::pulsar, "Pulsar", firstExisting({
|
||||
under(local, "Programs/Pulsar/Pulsar.exe"),
|
||||
under(program_files, "Pulsar/Pulsar.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::brackets, "Brackets", firstExisting({
|
||||
under(program_files, "Brackets/Brackets.exe"),
|
||||
under(program_files_x86, "Brackets/Brackets.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::lapce, "Lapce", firstExisting({
|
||||
under(local, "Programs/Lapce/Lapce.exe"),
|
||||
under(program_files, "Lapce/Lapce.exe"),
|
||||
under(user_profile, "scoop/apps/lapce/current/lapce.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::lite_xl, "Lite XL", firstExisting({
|
||||
under(program_files, "Lite XL/lite-xl.exe"),
|
||||
under(program_files_x86, "Lite XL/lite-xl.exe"),
|
||||
under(user_profile, "scoop/apps/lite-xl/current/lite-xl.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::geany, "Geany", firstExisting({
|
||||
under(program_files, "Geany/bin/geany.exe"),
|
||||
under(program_files_x86, "Geany/bin/geany.exe"),
|
||||
under(user_profile, "scoop/apps/geany/current/bin/geany.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::kate, "Kate", firstExisting({
|
||||
under(program_files, "Kate/bin/kate.exe"),
|
||||
under(program_files_x86, "Kate/bin/kate.exe"),
|
||||
under(user_profile, "scoop/apps/kate/current/bin/kate.exe"),
|
||||
}));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// C/C++ / Java / general IDEs
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
add(ExternalApplicationId::qt_creator, "Qt Creator", firstExisting({
|
||||
under(program_files, "Qt/Tools/QtCreator/bin/qtcreator.exe"),
|
||||
under(program_files_x86, "Qt/Tools/QtCreator/bin/qtcreator.exe"),
|
||||
under(user_profile, "scoop/apps/qt-creator/current/bin/qtcreator.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::codeblocks, "Code::Blocks", firstExisting({
|
||||
under(program_files, "CodeBlocks/codeblocks.exe"),
|
||||
under(program_files_x86, "CodeBlocks/codeblocks.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::dev_cpp, "Dev-C++", firstExisting({
|
||||
under(program_files, "Embarcadero/Dev-Cpp/devcpp.exe"),
|
||||
under(program_files_x86, "Embarcadero/Dev-Cpp/devcpp.exe"),
|
||||
under(program_files, "Dev-Cpp/devcpp.exe"),
|
||||
under(program_files_x86, "Dev-Cpp/devcpp.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::eclipse, "Eclipse", firstExisting({
|
||||
under(user_profile, "eclipse/java-latest-released/eclipse/eclipse.exe"),
|
||||
under(program_files, "Eclipse Foundation/eclipse/eclipse.exe"),
|
||||
under(program_files, "Eclipse/eclipse.exe"),
|
||||
under(local, "Programs/Eclipse/eclipse.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::netbeans, "NetBeans", firstExisting({
|
||||
under(program_files, "NetBeans-25/netbeans/bin/netbeans64.exe"),
|
||||
under(program_files, "NetBeans-24/netbeans/bin/netbeans64.exe"),
|
||||
under(program_files, "NetBeans-23/netbeans/bin/netbeans64.exe"),
|
||||
under(program_files, "NetBeans/netbeans/bin/netbeans64.exe"),
|
||||
under(program_files_x86, "NetBeans/netbeans/bin/netbeans.exe"),
|
||||
}));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Terminal editors
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
add(ExternalApplicationId::vim, "Vim", firstExisting({
|
||||
under(program_files, "Vim/vim91/gvim.exe"),
|
||||
under(program_files, "Vim/vim90/gvim.exe"),
|
||||
under(program_files_x86, "Vim/vim91/gvim.exe"),
|
||||
under(program_files_x86, "Vim/vim90/gvim.exe"),
|
||||
under(user_profile, "scoop/apps/vim/current/gvim.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::neovim, "Neovim", firstExisting({
|
||||
under(program_files, "Neovim/bin/nvim-qt.exe"),
|
||||
under(program_files, "Neovim/bin/nvim.exe"),
|
||||
under(local, "Programs/Neovim/bin/nvim-qt.exe"),
|
||||
under(local, "Programs/Neovim/bin/nvim.exe"),
|
||||
under(user_profile, "scoop/apps/neovim/current/bin/nvim.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::emacs, "Emacs", firstExisting({
|
||||
under(program_files, "Emacs/x86_64/bin/runemacs.exe"),
|
||||
under(program_files, "Emacs/bin/runemacs.exe"),
|
||||
under(program_files_x86, "Emacs/bin/runemacs.exe"),
|
||||
under(user_profile, "scoop/apps/emacs/current/bin/runemacs.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::helix, "Helix", firstExisting({
|
||||
under(program_files, "helix/hx.exe"),
|
||||
under(program_files_x86, "helix/hx.exe"),
|
||||
under(user_profile, "scoop/apps/helix/current/hx.exe"),
|
||||
}));
|
||||
}
|
||||
|
||||
const ExternalApplication *ApplicationManager::application(ExternalApplicationId id) const
|
||||
{
|
||||
const auto found = std::find_if(applications_.begin(), applications_.end(),
|
||||
[id](const ExternalApplication &application)
|
||||
{ return application.id == id; });
|
||||
{
|
||||
return application.id == id;
|
||||
});
|
||||
|
||||
return found == applications_.end() ? nullptr : &*found;
|
||||
}
|
||||
|
||||
ExternalApplicationId ApplicationManager::defaultApplication() const
|
||||
{
|
||||
const auto preferred = std::find_if(targets_.begin(), targets_.end(),
|
||||
[](const LaunchTarget &target)
|
||||
{
|
||||
return target.info.available &&
|
||||
target.info.id != ExternalApplicationId::file_explorer &&
|
||||
target.info.id != ExternalApplicationId::terminal &&
|
||||
target.info.id != ExternalApplicationId::git_bash &&
|
||||
target.info.id != ExternalApplicationId::wsl;
|
||||
});
|
||||
|
||||
if (preferred != targets_.end())
|
||||
return preferred->info.id;
|
||||
|
||||
const auto available = std::find_if(targets_.begin(), targets_.end(),
|
||||
[](const LaunchTarget &target)
|
||||
{ return target.info.available; });
|
||||
{
|
||||
return target.info.available;
|
||||
});
|
||||
|
||||
return available == targets_.end()
|
||||
? ExternalApplicationId::file_explorer
|
||||
: available->info.id;
|
||||
}
|
||||
|
||||
bool ApplicationManager::launch(ExternalApplicationId application,
|
||||
const std::filesystem::path &repository, std::string &error) const
|
||||
const std::filesystem::path &repository,
|
||||
std::string &error) const
|
||||
{
|
||||
const auto found = std::find_if(targets_.begin(), targets_.end(),
|
||||
[application](const LaunchTarget &target)
|
||||
{ return target.info.id == application; });
|
||||
{
|
||||
return target.info.id == application;
|
||||
});
|
||||
|
||||
if (found == targets_.end() || !found->info.available)
|
||||
{
|
||||
error = "The selected application is not installed";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::string> arguments = found->arguments;
|
||||
const std::string repository_path = repository.string();
|
||||
|
||||
if (application == ExternalApplicationId::git_bash && !arguments.empty())
|
||||
{
|
||||
arguments.back() += repository_path;
|
||||
}
|
||||
else
|
||||
{
|
||||
arguments.push_back(repository_path);
|
||||
}
|
||||
|
||||
const izo::ProcessResult result = izo::LaunchProcess({
|
||||
found->info.executable,
|
||||
std::move(arguments),
|
||||
repository,
|
||||
true,
|
||||
});
|
||||
|
||||
if (!result)
|
||||
{
|
||||
error = result.errorMessage.empty() ? "Unable to launch application" : result.errorMessage;
|
||||
return false;
|
||||
}
|
||||
|
||||
error = "Opened repository in " + found->info.name;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -7,15 +7,55 @@
|
||||
enum class ExternalApplicationId
|
||||
{
|
||||
visual_studio_code,
|
||||
visual_studio,
|
||||
visual_studio_code_insiders,
|
||||
vscodium,
|
||||
cursor,
|
||||
windsurf,
|
||||
trae,
|
||||
zed,
|
||||
antigravity,
|
||||
|
||||
visual_studio,
|
||||
|
||||
github_desktop,
|
||||
file_explorer,
|
||||
terminal,
|
||||
git_bash,
|
||||
wsl,
|
||||
|
||||
android_studio,
|
||||
|
||||
intellij_idea,
|
||||
clion,
|
||||
rider,
|
||||
pycharm,
|
||||
webstorm,
|
||||
phpstorm,
|
||||
rubymine,
|
||||
goland,
|
||||
datagrip,
|
||||
jetbrains_fleet,
|
||||
|
||||
sublime_text,
|
||||
notepad_plus_plus,
|
||||
atom,
|
||||
pulsar,
|
||||
brackets,
|
||||
lapce,
|
||||
lite_xl,
|
||||
geany,
|
||||
kate,
|
||||
|
||||
qt_creator,
|
||||
codeblocks,
|
||||
dev_cpp,
|
||||
eclipse,
|
||||
netbeans,
|
||||
|
||||
vim,
|
||||
neovim,
|
||||
emacs,
|
||||
helix,
|
||||
};
|
||||
|
||||
struct ExternalApplication
|
||||
@@ -46,4 +86,4 @@ private:
|
||||
|
||||
std::vector<LaunchTarget> targets_;
|
||||
std::vector<ExternalApplication> applications_;
|
||||
};
|
||||
};
|
||||
@@ -80,6 +80,25 @@ namespace
|
||||
return action;
|
||||
}
|
||||
|
||||
void populateRepositoryIdentity(RepositoryView &repository)
|
||||
{
|
||||
const char *workdir = git_repository_workdir(repository.repo);
|
||||
repository.path = workdir ? workdir : git_repository_path(repository.repo);
|
||||
std::filesystem::path path(repository.path);
|
||||
repository.name = path.filename().string();
|
||||
if (repository.name.empty())
|
||||
repository.name = path.parent_path().filename().string();
|
||||
|
||||
repository.branch = "detached";
|
||||
git_reference *head = nullptr;
|
||||
if (git_repository_head(&head, repository.repo) == 0)
|
||||
{
|
||||
if (const char *name = git_reference_shorthand(head))
|
||||
repository.branch = name;
|
||||
git_reference_free(head);
|
||||
}
|
||||
}
|
||||
|
||||
void addBadge(RepositoryView &repository, const git_oid *oid, RefBadge badge)
|
||||
{
|
||||
if (!oid)
|
||||
@@ -369,54 +388,65 @@ void GitManager::computeGraphLanes(RepositoryView &repository)
|
||||
{
|
||||
git_oid expected{};
|
||||
};
|
||||
|
||||
const auto lane_for_commit = [](std::vector<Lane> &lanes, const git_oid &oid) {
|
||||
const auto found = std::find_if(lanes.begin(), lanes.end(), [&oid](const Lane &lane)
|
||||
{ return git_oid_equal(&lane.expected, &oid) != 0; });
|
||||
if (found != lanes.end())
|
||||
return static_cast<size_t>(std::distance(lanes.begin(), found));
|
||||
lanes.push_back({oid});
|
||||
return lanes.size() - 1;
|
||||
};
|
||||
|
||||
const auto contains_oid = [](const std::vector<Lane> &lanes, const git_oid &oid) {
|
||||
return std::any_of(lanes.begin(), lanes.end(), [&oid](const Lane &lane)
|
||||
{ return git_oid_equal(&lane.expected, &oid) != 0; });
|
||||
};
|
||||
|
||||
std::vector<Lane> lanes;
|
||||
for (auto &commit : repository.commits)
|
||||
{
|
||||
auto current = std::find_if(lanes.begin(), lanes.end(), [&commit](const Lane &lane)
|
||||
{ return git_oid_equal(&lane.expected, &commit.oid) != 0; });
|
||||
if (current == lanes.end())
|
||||
{
|
||||
lanes.push_back({commit.oid});
|
||||
current = std::prev(lanes.end());
|
||||
}
|
||||
const size_t commit_lane = static_cast<size_t>(std::distance(lanes.begin(), current));
|
||||
size_t active_lane = commit_lane;
|
||||
size_t active_lane = lane_for_commit(lanes, commit.oid);
|
||||
const size_t commit_lane = active_lane;
|
||||
commit.lane = static_cast<int>(commit_lane);
|
||||
commit.graph_color = commit.lane;
|
||||
for (size_t duplicate = lanes.size(); duplicate-- > 0;)
|
||||
{
|
||||
if (duplicate != active_lane &&
|
||||
git_oid_equal(&lanes[duplicate].expected, &commit.oid) != 0)
|
||||
{
|
||||
lanes.erase(lanes.begin() + static_cast<std::ptrdiff_t>(duplicate));
|
||||
if (duplicate < active_lane)
|
||||
--active_lane;
|
||||
}
|
||||
}
|
||||
if (commit.parent_ids.empty())
|
||||
{
|
||||
lanes.erase(lanes.begin() + static_cast<std::ptrdiff_t>(active_lane));
|
||||
continue;
|
||||
}
|
||||
|
||||
lanes[active_lane].expected = commit.parent_ids.front();
|
||||
|
||||
size_t insert_lane = active_lane + 1;
|
||||
for (size_t parent = 1; parent < commit.parent_ids.size(); ++parent)
|
||||
{
|
||||
if (contains_oid(lanes, commit.parent_ids[parent]))
|
||||
continue;
|
||||
lanes.insert(lanes.begin() + static_cast<std::ptrdiff_t>(std::min(insert_lane, lanes.size())),
|
||||
{commit.parent_ids[parent]});
|
||||
++insert_lane;
|
||||
}
|
||||
|
||||
for (size_t duplicate = lanes.size(); duplicate-- > 0;)
|
||||
{
|
||||
if (duplicate != active_lane &&
|
||||
git_oid_equal(&lanes[duplicate].expected, &commit.parent_ids.front()) != 0)
|
||||
if (duplicate == active_lane)
|
||||
continue;
|
||||
if (git_oid_equal(&lanes[duplicate].expected, &commit.oid) != 0)
|
||||
{
|
||||
lanes.erase(lanes.begin() + static_cast<std::ptrdiff_t>(duplicate));
|
||||
if (duplicate < active_lane)
|
||||
--active_lane;
|
||||
}
|
||||
}
|
||||
for (size_t parent = 1; parent < commit.parent_ids.size(); ++parent)
|
||||
|
||||
for (size_t left = 0; left < lanes.size(); ++left)
|
||||
{
|
||||
const auto found = std::find_if(lanes.begin(), lanes.end(), [&commit, parent](const Lane &candidate)
|
||||
{ return git_oid_equal(&candidate.expected, &commit.parent_ids[parent]) != 0; });
|
||||
if (found == lanes.end())
|
||||
lanes.insert(lanes.begin() + static_cast<std::ptrdiff_t>(
|
||||
std::min(active_lane + parent, lanes.size())),
|
||||
{commit.parent_ids[parent]});
|
||||
for (size_t right = lanes.size(); right-- > left + 1;)
|
||||
{
|
||||
if (git_oid_equal(&lanes[left].expected, &lanes[right].expected) != 0)
|
||||
lanes.erase(lanes.begin() + static_cast<std::ptrdiff_t>(right));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -528,26 +558,15 @@ bool GitManager::loadRepositoryData(RepositoryView &repository, std::string &err
|
||||
repository.commits.clear();
|
||||
repository.selected_commit = 0;
|
||||
repository.scroll_to_commit = -1;
|
||||
repository.undo_action = {};
|
||||
repository.redo_action = {};
|
||||
if (repository.commit_walk)
|
||||
git_revwalk_free(repository.commit_walk);
|
||||
repository.commit_walk = nullptr;
|
||||
repository.history_exhausted = false;
|
||||
loadWorkingTree(repository);
|
||||
const char *workdir = git_repository_workdir(repository.repo);
|
||||
repository.path = workdir ? workdir : git_repository_path(repository.repo);
|
||||
std::filesystem::path path(repository.path);
|
||||
repository.name = path.filename().string();
|
||||
if (repository.name.empty())
|
||||
repository.name = path.parent_path().filename().string();
|
||||
populateRepositoryIdentity(repository);
|
||||
loadToolbarHistoryActions(repository);
|
||||
|
||||
git_reference *head = nullptr;
|
||||
if (git_repository_head(&head, repository.repo) == 0)
|
||||
{
|
||||
if (const char *name = git_reference_shorthand(head))
|
||||
repository.branch = name;
|
||||
git_reference_free(head);
|
||||
}
|
||||
readBranches(repository, GIT_BRANCH_LOCAL, repository.local_branches);
|
||||
readBranches(repository, GIT_BRANCH_REMOTE, repository.remote_branches);
|
||||
loadBranchDivergence(repository);
|
||||
@@ -672,7 +691,7 @@ bool GitManager::loadMoreCommits(RepositoryView &repository, size_t page_size, s
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GitManager::openRepository(RepositoryView &repository, const std::string &path, std::string &error)
|
||||
bool GitManager::openRepositoryHandle(RepositoryView &repository, const std::string &path, std::string &error)
|
||||
{
|
||||
git_repository *opened = nullptr;
|
||||
if (git_repository_open_ext(&opened, path.c_str(), 0, nullptr) != 0)
|
||||
@@ -682,6 +701,14 @@ bool GitManager::openRepository(RepositoryView &repository, const std::string &p
|
||||
}
|
||||
repository.close();
|
||||
repository.repo = opened;
|
||||
populateRepositoryIdentity(repository);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GitManager::openRepository(RepositoryView &repository, const std::string &path, std::string &error)
|
||||
{
|
||||
if (!openRepositoryHandle(repository, path, error))
|
||||
return false;
|
||||
return loadRepositoryData(repository, error);
|
||||
}
|
||||
|
||||
@@ -1250,6 +1277,27 @@ bool GitManager::checkoutBranch(RepositoryView &repository, const std::string &b
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GitManager::mergeBranch(RepositoryView &repository, const std::string &source_branch,
|
||||
const std::string &target_branch, std::string &error)
|
||||
{
|
||||
if (source_branch.empty() || target_branch.empty())
|
||||
{
|
||||
error = "Unable to merge branches";
|
||||
return false;
|
||||
}
|
||||
if (source_branch == target_branch)
|
||||
{
|
||||
error = "A branch cannot be merged into itself";
|
||||
return false;
|
||||
}
|
||||
if (repository.branch != target_branch && !checkoutBranch(repository, target_branch, error))
|
||||
return false;
|
||||
if (!runGit(repository, {"merge", "--no-ff", "--no-edit", source_branch}, "Branches merged", error))
|
||||
return false;
|
||||
error = "Merged " + source_branch + " into " + target_branch;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GitManager::updateSubmodule(RepositoryView &repository, const std::string &name, std::string &error)
|
||||
{
|
||||
git_submodule *submodule = nullptr;
|
||||
|
||||
@@ -1,69 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include "models/repository.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class GitManager
|
||||
{
|
||||
public:
|
||||
#pragma once
|
||||
|
||||
#include "models/repository.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class GitManager
|
||||
{
|
||||
public:
|
||||
GitManager();
|
||||
~GitManager();
|
||||
|
||||
bool openRepositoryHandle(RepositoryView &repository, const std::string &path, std::string &error);
|
||||
bool openRepository(RepositoryView &repository, const std::string &path, std::string &error);
|
||||
bool initializeRepository(RepositoryView &repository, const std::string &path,
|
||||
const std::string &initial_branch, std::string &error);
|
||||
bool cloneRepository(RepositoryView &repository, const std::string &url,
|
||||
const std::string &path, bool shallow, std::string &error);
|
||||
bool reload(RepositoryView &repository, std::string &error);
|
||||
bool cloneRepository(RepositoryView &repository, const std::string &url,
|
||||
const std::string &path, bool shallow, std::string &error);
|
||||
bool reload(RepositoryView &repository, std::string &error);
|
||||
bool loadMoreCommits(RepositoryView &repository, size_t page_size, std::string &error);
|
||||
bool checkoutBranch(RepositoryView &repository, const std::string &branch, std::string &error);
|
||||
bool mergeBranch(RepositoryView &repository, const std::string &source_branch,
|
||||
const std::string &target_branch, std::string &error);
|
||||
bool fetch(RepositoryView &repository, const std::string &remote, std::string &error);
|
||||
bool pull(RepositoryView &repository, int mode, std::string &error);
|
||||
bool push(RepositoryView &repository, std::string &error);
|
||||
bool pushBranch(RepositoryView &repository, const std::string &branch, std::string &error);
|
||||
bool stash(RepositoryView &repository, std::string &error);
|
||||
bool popStash(RepositoryView &repository, std::string &error);
|
||||
bool undoCommit(RepositoryView &repository, std::string &error);
|
||||
bool redoCommit(RepositoryView &repository, std::string &error);
|
||||
bool stageAll(RepositoryView &repository, std::string &error);
|
||||
bool unstageAll(RepositoryView &repository, std::string &error);
|
||||
bool stageFile(RepositoryView &repository, const std::string &path, std::string &error);
|
||||
bool unstageFile(RepositoryView &repository, const std::string &path, std::string &error);
|
||||
bool discardFile(RepositoryView &repository, const std::string &path, std::string &error);
|
||||
bool discardAll(RepositoryView &repository, std::string &error);
|
||||
bool commit(RepositoryView &repository, const std::string &summary,
|
||||
const std::string &description, bool amend, std::string &error);
|
||||
bool createBranch(RepositoryView &repository, const std::string &name,
|
||||
const std::string &start_point, bool checkout, std::string &error);
|
||||
bool createTag(RepositoryView &repository, const std::string &name,
|
||||
const std::string &target, std::string &error);
|
||||
bool addRemote(RepositoryView &repository, const std::string &name,
|
||||
const std::string &url, std::string &error);
|
||||
bool addWorktree(RepositoryView &repository, const std::string &path,
|
||||
const std::string &branch, std::string &error);
|
||||
bool addSubmodule(RepositoryView &repository, const std::string &url,
|
||||
const std::string &path, std::string &error);
|
||||
bool updateSubmodule(RepositoryView &repository, const std::string &name, std::string &error);
|
||||
std::string worktreePath(RepositoryView &repository, const std::string &name, std::string &error);
|
||||
bool loadCommitChanges(RepositoryView &repository, int commit_index, std::string &error);
|
||||
bool loadCommitFiles(RepositoryView &repository, int commit_index, std::string &error);
|
||||
bool captureGit(RepositoryView &repository, const std::vector<std::string> &arguments,
|
||||
std::string &output, std::string &error);
|
||||
bool applyPatch(RepositoryView &repository, const std::string &patch, bool cached,
|
||||
bool reverse, std::string &error);
|
||||
|
||||
private:
|
||||
static std::string lastError(const char *fallback);
|
||||
static std::string formatTime(git_time_t value, int offset_minutes);
|
||||
void loadToolbarHistoryActions(RepositoryView &repository);
|
||||
void readBranches(RepositoryView &repository, git_branch_t type, std::vector<std::string> &output);
|
||||
void loadBranchDivergence(RepositoryView &repository);
|
||||
void loadRefBadges(RepositoryView &repository);
|
||||
void computeGraphLanes(RepositoryView &repository);
|
||||
bool loadRepositoryData(RepositoryView &repository, std::string &error);
|
||||
void loadWorkingTree(RepositoryView &repository);
|
||||
bool prepareCredentials(RepositoryView &repository, std::string &error);
|
||||
bool runGit(RepositoryView &repository, const std::vector<std::string> &arguments,
|
||||
const char *success_message, std::string &message, bool reload = true);
|
||||
};
|
||||
bool pull(RepositoryView &repository, int mode, std::string &error);
|
||||
bool push(RepositoryView &repository, std::string &error);
|
||||
bool pushBranch(RepositoryView &repository, const std::string &branch, std::string &error);
|
||||
bool stash(RepositoryView &repository, std::string &error);
|
||||
bool popStash(RepositoryView &repository, std::string &error);
|
||||
bool undoCommit(RepositoryView &repository, std::string &error);
|
||||
bool redoCommit(RepositoryView &repository, std::string &error);
|
||||
bool stageAll(RepositoryView &repository, std::string &error);
|
||||
bool unstageAll(RepositoryView &repository, std::string &error);
|
||||
bool stageFile(RepositoryView &repository, const std::string &path, std::string &error);
|
||||
bool unstageFile(RepositoryView &repository, const std::string &path, std::string &error);
|
||||
bool discardFile(RepositoryView &repository, const std::string &path, std::string &error);
|
||||
bool discardAll(RepositoryView &repository, std::string &error);
|
||||
bool commit(RepositoryView &repository, const std::string &summary,
|
||||
const std::string &description, bool amend, std::string &error);
|
||||
bool createBranch(RepositoryView &repository, const std::string &name,
|
||||
const std::string &start_point, bool checkout, std::string &error);
|
||||
bool createTag(RepositoryView &repository, const std::string &name,
|
||||
const std::string &target, std::string &error);
|
||||
bool addRemote(RepositoryView &repository, const std::string &name,
|
||||
const std::string &url, std::string &error);
|
||||
bool addWorktree(RepositoryView &repository, const std::string &path,
|
||||
const std::string &branch, std::string &error);
|
||||
bool addSubmodule(RepositoryView &repository, const std::string &url,
|
||||
const std::string &path, std::string &error);
|
||||
bool updateSubmodule(RepositoryView &repository, const std::string &name, std::string &error);
|
||||
std::string worktreePath(RepositoryView &repository, const std::string &name, std::string &error);
|
||||
bool loadCommitChanges(RepositoryView &repository, int commit_index, std::string &error);
|
||||
bool loadCommitFiles(RepositoryView &repository, int commit_index, std::string &error);
|
||||
bool captureGit(RepositoryView &repository, const std::vector<std::string> &arguments,
|
||||
std::string &output, std::string &error);
|
||||
bool applyPatch(RepositoryView &repository, const std::string &patch, bool cached,
|
||||
bool reverse, std::string &error);
|
||||
|
||||
private:
|
||||
static std::string lastError(const char *fallback);
|
||||
static std::string formatTime(git_time_t value, int offset_minutes);
|
||||
void loadToolbarHistoryActions(RepositoryView &repository);
|
||||
void readBranches(RepositoryView &repository, git_branch_t type, std::vector<std::string> &output);
|
||||
void loadBranchDivergence(RepositoryView &repository);
|
||||
void loadRefBadges(RepositoryView &repository);
|
||||
void computeGraphLanes(RepositoryView &repository);
|
||||
bool loadRepositoryData(RepositoryView &repository, std::string &error);
|
||||
void loadWorkingTree(RepositoryView &repository);
|
||||
bool prepareCredentials(RepositoryView &repository, std::string &error);
|
||||
bool runGit(RepositoryView &repository, const std::vector<std::string> &arguments,
|
||||
const char *success_message, std::string &message, bool reload = true);
|
||||
};
|
||||
|
||||
@@ -65,6 +65,8 @@ void UserData::load()
|
||||
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))
|
||||
@@ -76,6 +78,20 @@ void UserData::load()
|
||||
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))
|
||||
{
|
||||
@@ -117,6 +133,8 @@ void UserData::load()
|
||||
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
|
||||
@@ -133,6 +151,8 @@ void UserData::load()
|
||||
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))
|
||||
@@ -144,6 +164,20 @@ void UserData::load()
|
||||
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))
|
||||
{
|
||||
@@ -185,6 +219,8 @@ void UserData::load()
|
||||
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
|
||||
@@ -299,11 +335,18 @@ void UserData::save() const
|
||||
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);
|
||||
|
||||
ikv_node_t *history = ikv_object_add_array(root, "recently_closed", IKV_STRING);
|
||||
for (const auto &path : recently_closed_)
|
||||
|
||||
@@ -19,12 +19,18 @@ public:
|
||||
[[nodiscard]] float sidebarWidth() const { return sidebar_width_; }
|
||||
[[nodiscard]] float detailsWidth() const { return details_width_; }
|
||||
[[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]] int pullMode() const { return pull_mode_; }
|
||||
[[nodiscard]] int zoomPercent() const { return zoom_percent_; }
|
||||
|
||||
void setSidebarWidth(float width) { sidebar_width_ = width; }
|
||||
void setDetailsWidth(float width) { details_width_ = width; }
|
||||
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 setPullMode(int mode)
|
||||
{
|
||||
pull_mode_ = mode;
|
||||
@@ -54,6 +60,9 @@ private:
|
||||
float sidebar_width_ = 230.0f;
|
||||
float details_width_ = 368.0f;
|
||||
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};
|
||||
std::array<float, 4> commit_table_column_widths_ = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
int pull_mode_ = 1;
|
||||
int zoom_percent_ = 100;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <git2.h>
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
@@ -85,6 +86,8 @@ struct RepositoryView {
|
||||
std::vector<WorkingFile> working_files;
|
||||
int selected_commit = 0;
|
||||
int scroll_to_commit = -1;
|
||||
std::chrono::steady_clock::time_point last_background_refresh{};
|
||||
bool pending_background_refresh = false;
|
||||
ToolbarHistoryAction undo_action;
|
||||
ToolbarHistoryAction redo_action;
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <initializer_list>
|
||||
#include <numeric>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
|
||||
@@ -152,23 +153,23 @@ bool isMacroName(std::string_view word) {
|
||||
return has_letter && word.size() > 1;
|
||||
}
|
||||
|
||||
constexpr ImU32 syntax_normal = IM_COL32(218, 221, 226, 255);
|
||||
constexpr ImU32 syntax_keyword = IM_COL32(198, 139, 230, 255);
|
||||
constexpr ImU32 syntax_type = IM_COL32(91, 198, 190, 255);
|
||||
constexpr ImU32 syntax_string = IM_COL32(226, 166, 140, 255);
|
||||
constexpr ImU32 syntax_number = IM_COL32(181, 206, 126, 255);
|
||||
constexpr ImU32 syntax_comment = IM_COL32(112, 153, 105, 255);
|
||||
constexpr ImU32 syntax_function = IM_COL32(220, 199, 128, 255);
|
||||
constexpr ImU32 syntax_preprocessor = IM_COL32(205, 157, 222, 255);
|
||||
constexpr ImU32 syntax_member = IM_COL32(122, 184, 225, 255);
|
||||
constexpr ImU32 syntax_builtin = IM_COL32(103, 172, 232, 255);
|
||||
constexpr ImU32 syntax_constant = IM_COL32(214, 139, 102, 255);
|
||||
constexpr ImU32 syntax_operator = IM_COL32(105, 180, 210, 255);
|
||||
constexpr ImU32 syntax_decorator = IM_COL32(226, 190, 105, 255);
|
||||
constexpr ImU32 syntax_macro = IM_COL32(215, 128, 180, 255);
|
||||
|
||||
[[maybe_unused]] void drawSyntaxText(ImDrawList* draw, ImVec2 position, const std::string& text,
|
||||
SyntaxLanguage language, SyntaxState& state) {
|
||||
constexpr ImU32 normal = IM_COL32(218, 221, 226, 255);
|
||||
constexpr ImU32 keyword = IM_COL32(198, 139, 230, 255);
|
||||
constexpr ImU32 type = IM_COL32(91, 198, 190, 255);
|
||||
constexpr ImU32 string = IM_COL32(226, 166, 140, 255);
|
||||
constexpr ImU32 number = IM_COL32(181, 206, 126, 255);
|
||||
constexpr ImU32 comment = IM_COL32(112, 153, 105, 255);
|
||||
constexpr ImU32 function = IM_COL32(220, 199, 128, 255);
|
||||
constexpr ImU32 preprocessor = IM_COL32(205, 157, 222, 255);
|
||||
constexpr ImU32 member = IM_COL32(122, 184, 225, 255);
|
||||
constexpr ImU32 builtin = IM_COL32(103, 172, 232, 255);
|
||||
constexpr ImU32 constant = IM_COL32(214, 139, 102, 255);
|
||||
constexpr ImU32 operator_color = IM_COL32(105, 180, 210, 255);
|
||||
constexpr ImU32 decorator = IM_COL32(226, 190, 105, 255);
|
||||
constexpr ImU32 macro = IM_COL32(215, 128, 180, 255);
|
||||
|
||||
const auto drawSpan = [&](size_t begin, size_t end, ImU32 color) {
|
||||
if (end <= begin) return;
|
||||
const char* first = text.data() + begin;
|
||||
@@ -177,15 +178,15 @@ bool isMacroName(std::string_view word) {
|
||||
position.x += ImGui::CalcTextSize(first, last, false).x;
|
||||
};
|
||||
if (language == SyntaxLanguage::plain) {
|
||||
drawSpan(0, text.size(), normal);
|
||||
drawSpan(0, text.size(), syntax_normal);
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t first_non_space = text.find_first_not_of(" \t");
|
||||
if ((language == SyntaxLanguage::c || language == SyntaxLanguage::cpp || language == SyntaxLanguage::csharp) &&
|
||||
first_non_space != std::string::npos && text[first_non_space] == '#') {
|
||||
drawSpan(0, first_non_space, normal);
|
||||
drawSpan(first_non_space, text.size(), preprocessor);
|
||||
drawSpan(0, first_non_space, syntax_normal);
|
||||
drawSpan(first_non_space, text.size(), syntax_preprocessor);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -194,16 +195,16 @@ bool isMacroName(std::string_view word) {
|
||||
while (cursor < text.size()) {
|
||||
if (state.continuation == SyntaxContinuation::block_comment) {
|
||||
const size_t end = text.find("*/", cursor);
|
||||
if (end == std::string::npos) { drawSpan(cursor, text.size(), comment); return; }
|
||||
drawSpan(cursor, end + 2, comment);
|
||||
if (end == std::string::npos) { drawSpan(cursor, text.size(), syntax_comment); return; }
|
||||
drawSpan(cursor, end + 2, syntax_comment);
|
||||
cursor = end + 2;
|
||||
state.continuation = SyntaxContinuation::none;
|
||||
continue;
|
||||
}
|
||||
if (state.continuation == SyntaxContinuation::lua_comment) {
|
||||
const size_t end = text.find("]]", cursor);
|
||||
if (end == std::string::npos) { drawSpan(cursor, text.size(), comment); return; }
|
||||
drawSpan(cursor, end + 2, comment);
|
||||
if (end == std::string::npos) { drawSpan(cursor, text.size(), syntax_comment); return; }
|
||||
drawSpan(cursor, end + 2, syntax_comment);
|
||||
cursor = end + 2;
|
||||
state.continuation = SyntaxContinuation::none;
|
||||
continue;
|
||||
@@ -212,16 +213,16 @@ bool isMacroName(std::string_view word) {
|
||||
state.continuation == SyntaxContinuation::python_double) {
|
||||
const std::string_view delimiter = state.continuation == SyntaxContinuation::python_single ? "'''" : "\"\"\"";
|
||||
const size_t end = text.find(delimiter, cursor);
|
||||
if (end == std::string::npos) { drawSpan(cursor, text.size(), string); return; }
|
||||
drawSpan(cursor, end + delimiter.size(), string);
|
||||
if (end == std::string::npos) { drawSpan(cursor, text.size(), syntax_string); return; }
|
||||
drawSpan(cursor, end + delimiter.size(), syntax_string);
|
||||
cursor = end + delimiter.size();
|
||||
state.continuation = SyntaxContinuation::none;
|
||||
continue;
|
||||
}
|
||||
if (state.continuation == SyntaxContinuation::template_string) {
|
||||
const size_t end = text.find('`', cursor);
|
||||
if (end == std::string::npos) { drawSpan(cursor, text.size(), string); return; }
|
||||
drawSpan(cursor, end + 1, string);
|
||||
if (end == std::string::npos) { drawSpan(cursor, text.size(), syntax_string); return; }
|
||||
drawSpan(cursor, end + 1, syntax_string);
|
||||
cursor = end + 1;
|
||||
state.continuation = SyntaxContinuation::none;
|
||||
continue;
|
||||
@@ -230,37 +231,37 @@ bool isMacroName(std::string_view word) {
|
||||
const bool slash_comments = language == SyntaxLanguage::c || language == SyntaxLanguage::cpp ||
|
||||
language == SyntaxLanguage::csharp || language == SyntaxLanguage::javascript;
|
||||
if (slash_comments && text.compare(cursor, 2, "//") == 0) {
|
||||
drawSpan(cursor, text.size(), comment);
|
||||
drawSpan(cursor, text.size(), syntax_comment);
|
||||
return;
|
||||
}
|
||||
if (slash_comments && text.compare(cursor, 2, "/*") == 0) {
|
||||
const size_t end = text.find("*/", cursor + 2);
|
||||
if (end == std::string::npos) {
|
||||
drawSpan(cursor, text.size(), comment);
|
||||
drawSpan(cursor, text.size(), syntax_comment);
|
||||
state.continuation = SyntaxContinuation::block_comment;
|
||||
return;
|
||||
}
|
||||
drawSpan(cursor, end + 2, comment);
|
||||
drawSpan(cursor, end + 2, syntax_comment);
|
||||
cursor = end + 2;
|
||||
continue;
|
||||
}
|
||||
if (language == SyntaxLanguage::python && text[cursor] == '#') {
|
||||
drawSpan(cursor, text.size(), comment);
|
||||
drawSpan(cursor, text.size(), syntax_comment);
|
||||
return;
|
||||
}
|
||||
if (language == SyntaxLanguage::lua && text.compare(cursor, 4, "--[[") == 0) {
|
||||
const size_t end = text.find("]]", cursor + 4);
|
||||
if (end == std::string::npos) {
|
||||
drawSpan(cursor, text.size(), comment);
|
||||
drawSpan(cursor, text.size(), syntax_comment);
|
||||
state.continuation = SyntaxContinuation::lua_comment;
|
||||
return;
|
||||
}
|
||||
drawSpan(cursor, end + 2, comment);
|
||||
drawSpan(cursor, end + 2, syntax_comment);
|
||||
cursor = end + 2;
|
||||
continue;
|
||||
}
|
||||
if (language == SyntaxLanguage::lua && text.compare(cursor, 2, "--") == 0) {
|
||||
drawSpan(cursor, text.size(), comment);
|
||||
drawSpan(cursor, text.size(), syntax_comment);
|
||||
return;
|
||||
}
|
||||
if (language == SyntaxLanguage::python &&
|
||||
@@ -268,11 +269,11 @@ bool isMacroName(std::string_view word) {
|
||||
const std::string_view delimiter(text.data() + cursor, 3);
|
||||
const size_t end = text.find(delimiter, cursor + 3);
|
||||
if (end == std::string::npos) {
|
||||
drawSpan(cursor, text.size(), string);
|
||||
drawSpan(cursor, text.size(), syntax_string);
|
||||
state.continuation = delimiter[0] == '\'' ? SyntaxContinuation::python_single : SyntaxContinuation::python_double;
|
||||
return;
|
||||
}
|
||||
drawSpan(cursor, end + 3, string);
|
||||
drawSpan(cursor, end + 3, syntax_string);
|
||||
cursor = end + 3;
|
||||
continue;
|
||||
}
|
||||
@@ -287,7 +288,7 @@ bool isMacroName(std::string_view word) {
|
||||
if (text[end] != '\\') escaped = false;
|
||||
}
|
||||
const bool closed = end <= text.size() && end > cursor + 1 && text[end - 1] == quote;
|
||||
drawSpan(cursor, end, string);
|
||||
drawSpan(cursor, end, syntax_string);
|
||||
if (quote == '`' && !closed) state.continuation = SyntaxContinuation::template_string;
|
||||
cursor = end;
|
||||
continue;
|
||||
@@ -296,7 +297,7 @@ bool isMacroName(std::string_view word) {
|
||||
size_t end = cursor + 1;
|
||||
while (end < text.size() && (std::isalnum(static_cast<unsigned char>(text[end])) ||
|
||||
text[end] == '.' || text[end] == '_')) ++end;
|
||||
drawSpan(cursor, end, number);
|
||||
drawSpan(cursor, end, syntax_number);
|
||||
cursor = end;
|
||||
continue;
|
||||
}
|
||||
@@ -305,7 +306,7 @@ bool isMacroName(std::string_view word) {
|
||||
size_t end = cursor + 2;
|
||||
while (end < text.size() && (std::isalnum(static_cast<unsigned char>(text[end])) ||
|
||||
text[end] == '_' || text[end] == '.')) ++end;
|
||||
drawSpan(cursor, end, decorator);
|
||||
drawSpan(cursor, end, syntax_decorator);
|
||||
cursor = end;
|
||||
continue;
|
||||
}
|
||||
@@ -328,11 +329,11 @@ bool isMacroName(std::string_view word) {
|
||||
(next >= text.size() || text[next] != '(');
|
||||
const bool likely_type = declared_name || isTypeWord(language, word) ||
|
||||
capitalized_type;
|
||||
const ImU32 color = isLiteral(word) ? constant : isKeyword(language, word) ? keyword :
|
||||
declared_function ? function : declared_name || likely_type ? type :
|
||||
isMacroName(word) ? macro : isBuiltin(language, word) ? builtin : member_access ?
|
||||
(next < text.size() && text[next] == '(' ? function : member) :
|
||||
next < text.size() && text[next] == '(' ? function : normal;
|
||||
const ImU32 color = isLiteral(word) ? syntax_constant : isKeyword(language, word) ? syntax_keyword :
|
||||
declared_function ? syntax_function : declared_name || likely_type ? syntax_type :
|
||||
isMacroName(word) ? syntax_macro : isBuiltin(language, word) ? syntax_builtin : member_access ?
|
||||
(next < text.size() && text[next] == '(' ? syntax_function : syntax_member) :
|
||||
next < text.size() && text[next] == '(' ? syntax_function : syntax_normal;
|
||||
drawSpan(cursor, end, color);
|
||||
previous_word = word;
|
||||
cursor = end;
|
||||
@@ -342,7 +343,7 @@ bool isMacroName(std::string_view word) {
|
||||
size_t end = cursor + 1;
|
||||
while (end < text.size() &&
|
||||
std::string_view("+-*/%=!<>&|^~?:").find(text[end]) != std::string_view::npos) ++end;
|
||||
drawSpan(cursor, end, operator_color);
|
||||
drawSpan(cursor, end, syntax_operator);
|
||||
cursor = end;
|
||||
continue;
|
||||
}
|
||||
@@ -357,7 +358,7 @@ bool isMacroName(std::string_view word) {
|
||||
if (token_start) break;
|
||||
++end;
|
||||
}
|
||||
drawSpan(cursor, end, normal);
|
||||
drawSpan(cursor, end, syntax_normal);
|
||||
cursor = end;
|
||||
}
|
||||
}
|
||||
@@ -372,58 +373,189 @@ bool compactButton(const char* label, bool active = false) {
|
||||
return clicked;
|
||||
}
|
||||
|
||||
std::string joinLines(const std::vector<std::string>& lines) {
|
||||
std::string text;
|
||||
for (size_t index = 0; index < lines.size(); ++index) {
|
||||
if (index) text += '\n';
|
||||
text += lines[index];
|
||||
bool toolbarSegmentButton(const char* label, bool active, float width) {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f);
|
||||
if (active) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.13f, 0.25f, 0.43f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.15f, 0.29f, 0.49f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.12f, 0.22f, 0.39f, 1.0f));
|
||||
}
|
||||
return text;
|
||||
const bool clicked = ImGui::Button(label, {width, 0.0f});
|
||||
if (active) ImGui::PopStyleColor(3);
|
||||
ImGui::PopStyleVar();
|
||||
return clicked;
|
||||
}
|
||||
|
||||
bool drawSelectableTextBlock(const char* id, const std::string& text, const ImVec2& size) {
|
||||
std::vector<char> buffer(text.begin(), text.end());
|
||||
buffer.push_back('\0');
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.06f, 0.07f, 0.09f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.18f, 0.20f, 0.24f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {4.0f, 4.0f});
|
||||
const bool changed = ImGui::InputTextMultiline(id, buffer.data(), buffer.size(), size,
|
||||
ImGuiInputTextFlags_ReadOnly | ImGuiInputTextFlags_AllowTabInput);
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor(2);
|
||||
return changed;
|
||||
int wrappedLineCount(std::string_view text, int wrap_columns) {
|
||||
if (wrap_columns <= 0 || static_cast<int>(text.size()) <= wrap_columns) return 1;
|
||||
int lines = 0;
|
||||
size_t cursor = 0;
|
||||
while (cursor < text.size()) {
|
||||
size_t remaining = text.size() - cursor;
|
||||
if (remaining <= static_cast<size_t>(wrap_columns)) {
|
||||
++lines;
|
||||
break;
|
||||
}
|
||||
size_t split = cursor + static_cast<size_t>(wrap_columns);
|
||||
size_t break_at = text.rfind(' ', split);
|
||||
if (break_at == std::string_view::npos || break_at < cursor + static_cast<size_t>(wrap_columns / 3))
|
||||
break_at = split;
|
||||
cursor = break_at;
|
||||
while (cursor < text.size() && text[cursor] == ' ') ++cursor;
|
||||
++lines;
|
||||
}
|
||||
return std::max(lines, 1);
|
||||
}
|
||||
|
||||
struct MinimapEntry {
|
||||
size_t length = 0;
|
||||
ImU32 color = IM_COL32(120, 126, 136, 255);
|
||||
struct MinimapSegment {
|
||||
float weight = 0.0f;
|
||||
ImU32 color = syntax_normal;
|
||||
};
|
||||
|
||||
void drawMinimap(const std::vector<MinimapEntry>& entries, float scale,
|
||||
float rendered_content_height, float visible_start, float visible_height) {
|
||||
struct MinimapEntry {
|
||||
std::vector<MinimapSegment> segments;
|
||||
float units = 1.0f;
|
||||
};
|
||||
|
||||
void addMinimapSegment(std::vector<MinimapSegment>& segments, float weight, ImU32 color) {
|
||||
if (weight <= 0.0f) return;
|
||||
if (!segments.empty() && segments.back().color == color) {
|
||||
segments.back().weight += weight;
|
||||
return;
|
||||
}
|
||||
segments.push_back({weight, color});
|
||||
}
|
||||
|
||||
std::vector<MinimapSegment> buildMinimapSegments(const std::string& text, SyntaxLanguage language) {
|
||||
std::vector<MinimapSegment> segments;
|
||||
if (text.empty()) {
|
||||
segments.push_back({0.10f, IM_COL32(0, 0, 0, 0)});
|
||||
return segments;
|
||||
}
|
||||
size_t cursor = 0;
|
||||
bool seen_identifier = false;
|
||||
while (cursor < text.size()) {
|
||||
const unsigned char value = static_cast<unsigned char>(text[cursor]);
|
||||
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));
|
||||
cursor = end;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((language == SyntaxLanguage::c || language == SyntaxLanguage::cpp || language == SyntaxLanguage::csharp) &&
|
||||
cursor + 1 < text.size() && text[cursor] == '/' && text[cursor + 1] == '/') {
|
||||
addMinimapSegment(segments, std::max(0.18f, static_cast<float>(text.size() - cursor) * 0.42f), syntax_comment);
|
||||
break;
|
||||
}
|
||||
if (language == SyntaxLanguage::python && text[cursor] == '#') {
|
||||
addMinimapSegment(segments, std::max(0.18f, static_cast<float>(text.size() - cursor) * 0.42f), syntax_comment);
|
||||
break;
|
||||
}
|
||||
if (language == SyntaxLanguage::lua && cursor + 1 < text.size() && text[cursor] == '-' && text[cursor + 1] == '-') {
|
||||
addMinimapSegment(segments, std::max(0.18f, static_cast<float>(text.size() - cursor) * 0.42f), syntax_comment);
|
||||
break;
|
||||
}
|
||||
|
||||
if (text[cursor] == '"' || text[cursor] == '\'' || (language == SyntaxLanguage::javascript && text[cursor] == '`')) {
|
||||
const char delimiter = text[cursor];
|
||||
size_t end = cursor + 1;
|
||||
while (end < text.size()) {
|
||||
if (text[end] == delimiter && text[end - 1] != '\\') {
|
||||
++end;
|
||||
break;
|
||||
}
|
||||
++end;
|
||||
}
|
||||
addMinimapSegment(segments, std::max(0.10f, static_cast<float>(end - cursor) * 0.34f), syntax_string);
|
||||
cursor = end;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std::isdigit(value)) {
|
||||
size_t end = cursor + 1;
|
||||
while (end < text.size() && (std::isalnum(static_cast<unsigned char>(text[end])) || text[end] == '.' || text[end] == '_')) ++end;
|
||||
addMinimapSegment(segments, std::max(0.08f, static_cast<float>(end - cursor) * 0.28f), syntax_number);
|
||||
cursor = end;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std::isalpha(value) || text[cursor] == '_') {
|
||||
size_t end = cursor + 1;
|
||||
while (end < text.size() && (std::isalnum(static_cast<unsigned char>(text[end])) || text[end] == '_')) ++end;
|
||||
const std::string_view word(text.data() + cursor, end - cursor);
|
||||
ImU32 color = syntax_normal;
|
||||
if (isKeyword(language, word)) color = syntax_keyword;
|
||||
else if (isTypeWord(language, word)) color = syntax_type;
|
||||
else if (isBuiltin(language, word)) color = syntax_builtin;
|
||||
else if (isLiteral(word)) color = syntax_constant;
|
||||
else if (word.front() == '@') color = syntax_decorator;
|
||||
else if (isMacroName(word)) color = syntax_macro;
|
||||
else if (!seen_identifier && (isDeclarationKeyword(word) || isFunctionDeclarationKeyword(word)))
|
||||
color = syntax_keyword;
|
||||
else if (!seen_identifier && cursor > 0 && text[cursor - 1] == '.') color = syntax_member;
|
||||
else if (end < text.size() && text[end] == '(') color = syntax_function;
|
||||
addMinimapSegment(segments, std::max(0.08f, static_cast<float>(end - cursor) * 0.26f), color);
|
||||
seen_identifier = true;
|
||||
cursor = end;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std::string_view("+-*/%=!<>&|^~?:#").find(text[cursor]) != std::string_view::npos) {
|
||||
size_t end = cursor + 1;
|
||||
while (end < text.size() && std::string_view("+-*/%=!<>&|^~?:#").find(text[end]) != std::string_view::npos) ++end;
|
||||
const ImU32 color = text[cursor] == '#' ? syntax_preprocessor : syntax_operator;
|
||||
addMinimapSegment(segments, std::max(0.06f, static_cast<float>(end - cursor) * 0.22f), color);
|
||||
cursor = end;
|
||||
continue;
|
||||
}
|
||||
|
||||
addMinimapSegment(segments, 0.05f, syntax_normal);
|
||||
++cursor;
|
||||
}
|
||||
if (segments.empty()) segments.push_back({0.10f, syntax_normal});
|
||||
return segments;
|
||||
}
|
||||
|
||||
void drawMinimap(const std::vector<MinimapEntry>& entries, const ImVec2& size, float scale,
|
||||
float rendered_content_height, float visible_start, float visible_height, float max_scroll_y) {
|
||||
const ImVec2 minimum = ImGui::GetCursorScreenPos();
|
||||
const ImVec2 size = ImGui::GetContentRegionAvail();
|
||||
ImGui::InvisibleButton("##code_minimap", size);
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
const bool active = ImGui::IsItemActive();
|
||||
ImDrawList* draw = ImGui::GetWindowDrawList();
|
||||
draw->AddRectFilled(minimum, {minimum.x + size.x, minimum.y + size.y}, IM_COL32(38, 40, 47, 255));
|
||||
draw->AddRectFilled({minimum.x + size.x - scaled(10.0f, scale), minimum.y},
|
||||
{minimum.x + size.x, minimum.y + size.y}, IM_COL32(64, 66, 74, 255));
|
||||
draw->AddRectFilled(minimum, {minimum.x + size.x, minimum.y + size.y}, IM_COL32(31, 33, 39, 235), scaled(4.0f, scale));
|
||||
draw->AddRect(minimum, {minimum.x + size.x, minimum.y + size.y},
|
||||
hovered || active ? IM_COL32(82, 90, 103, 255) : IM_COL32(56, 61, 71, 255), scaled(4.0f, scale));
|
||||
|
||||
if (entries.empty() || size.y <= 2.0f) return;
|
||||
|
||||
const float content_left = minimum.x + scaled(4.0f, scale);
|
||||
const float content_right = minimum.x + size.x - scaled(14.0f, scale);
|
||||
const float content_left = minimum.x + scaled(5.0f, scale);
|
||||
const float content_right = minimum.x + size.x - scaled(5.0f, scale);
|
||||
const float content_width = std::max(1.0f, content_right - content_left);
|
||||
const float line_step = size.y / static_cast<float>(entries.size());
|
||||
const float line_height = std::max(1.0f, std::min(scaled(2.0f, scale), line_step));
|
||||
constexpr float max_reference_length = 160.0f;
|
||||
for (size_t index = 0; index < entries.size(); ++index) {
|
||||
const float normalized = std::clamp(static_cast<float>(entries[index].length) / max_reference_length, 0.08f, 1.0f);
|
||||
const float width = content_width * normalized;
|
||||
const float y = minimum.y + index * line_step;
|
||||
draw->AddRectFilled({content_left, y}, {content_left + width, y + line_height},
|
||||
entries[index].color, scaled(1.0f, scale));
|
||||
float total_units = 0.0f;
|
||||
for (const MinimapEntry& entry : entries) total_units += std::max(0.10f, entry.units);
|
||||
total_units = std::max(1.0f, total_units);
|
||||
const float unit_height = size.y / total_units;
|
||||
const float line_height = std::max(1.0f, std::min(scaled(3.2f, scale), unit_height));
|
||||
float y = minimum.y;
|
||||
for (const MinimapEntry& entry : entries) {
|
||||
const float entry_units = std::max(0.10f, entry.units);
|
||||
const float total_weight = std::max(0.10f, std::accumulate(entry.segments.begin(), entry.segments.end(), 0.0f,
|
||||
[](float sum, const MinimapSegment& segment) { return sum + std::max(0.0f, segment.weight); }));
|
||||
float x = content_left;
|
||||
for (const MinimapSegment& segment : entry.segments) {
|
||||
if (segment.color == IM_COL32(0, 0, 0, 0)) {
|
||||
x += content_width * (segment.weight / total_weight);
|
||||
continue;
|
||||
}
|
||||
const float width = std::max(scaled(1.0f, scale), content_width * (segment.weight / total_weight));
|
||||
draw->AddRectFilled({x, y}, {std::min(content_right, x + width), y + line_height},
|
||||
segment.color, scaled(1.0f, scale));
|
||||
x += width;
|
||||
}
|
||||
y += entry_units * unit_height;
|
||||
}
|
||||
|
||||
const float content_height = std::max(rendered_content_height, 1.0f);
|
||||
@@ -432,27 +564,58 @@ void drawMinimap(const std::vector<MinimapEntry>& entries, float scale,
|
||||
const float viewport_height = std::clamp(size.y * (clamped_visible_height / content_height),
|
||||
scaled(18.0f, scale), size.y);
|
||||
const float viewport_y = minimum.y + size.y * (clamped_visible_start / content_height);
|
||||
if ((hovered || active) && ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
||||
const float mouse_ratio = std::clamp((ImGui::GetIO().MousePos.y - minimum.y) / std::max(1.0f, size.y), 0.0f, 1.0f);
|
||||
const float target = std::clamp(mouse_ratio * content_height - clamped_visible_height * 0.5f, 0.0f, max_scroll_y);
|
||||
ImGui::SetScrollY(target);
|
||||
}
|
||||
draw->AddRectFilled({minimum.x + scaled(1.0f, scale), viewport_y},
|
||||
{minimum.x + size.x - scaled(11.0f, scale), viewport_y + viewport_height},
|
||||
IM_COL32(120, 146, 198, 45), scaled(2.0f, scale));
|
||||
{minimum.x + size.x - scaled(1.0f, scale), viewport_y + viewport_height},
|
||||
IM_COL32(120, 146, 198, hovered || active ? 62 : 42), scaled(2.0f, scale));
|
||||
draw->AddRect({minimum.x + scaled(1.0f, scale), viewport_y},
|
||||
{minimum.x + size.x - scaled(11.0f, scale), viewport_y + viewport_height},
|
||||
IM_COL32(120, 146, 198, 160), scaled(2.0f, scale));
|
||||
{minimum.x + size.x - scaled(1.0f, scale), viewport_y + viewport_height},
|
||||
IM_COL32(120, 146, 198, 185), scaled(2.0f, scale));
|
||||
}
|
||||
|
||||
void drawCodeLine(const std::string& text, SyntaxLanguage language, SyntaxState& syntax,
|
||||
float scale, ImU32 background = IM_COL32(0, 0, 0, 0), float left_gutter = 0.0f,
|
||||
float minimum_width = 0.0f) {
|
||||
const float row_height = scaled(21.0f, scale);
|
||||
float minimum_width = 0.0f, float custom_row_height = 0.0f, int wrap_columns = 0) {
|
||||
const float base_row_height = scaled(21.0f, scale);
|
||||
const int visual_lines = wrappedLineCount(text, wrap_columns);
|
||||
const float row_height = custom_row_height > 0.0f ? custom_row_height : base_row_height * static_cast<float>(visual_lines);
|
||||
const ImVec2 start = ImGui::GetCursorScreenPos();
|
||||
const float width = std::max(minimum_width, ImGui::CalcTextSize(text.c_str()).x + left_gutter + scaled(12.0f, scale));
|
||||
const float width = minimum_width > 0.0f ? minimum_width :
|
||||
std::max(minimum_width, ImGui::CalcTextSize(text.c_str()).x + left_gutter + scaled(12.0f, scale));
|
||||
ImGui::InvisibleButton("##code_line", {width, row_height});
|
||||
const ImVec2 minimum = ImGui::GetItemRectMin();
|
||||
const ImVec2 maximum = ImGui::GetItemRectMax();
|
||||
ImDrawList* draw = ImGui::GetWindowDrawList();
|
||||
if ((background & IM_COL32_A_MASK) != 0)
|
||||
draw->AddRectFilled(minimum, maximum, background);
|
||||
drawSyntaxText(draw, {start.x + left_gutter, start.y + scaled(2.0f, scale)}, text, language, syntax);
|
||||
if (wrap_columns <= 0 || static_cast<int>(text.size()) <= wrap_columns) {
|
||||
drawSyntaxText(draw, {start.x + left_gutter, start.y + scaled(2.0f, scale)}, text, language, syntax);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t cursor = 0;
|
||||
int line_index = 0;
|
||||
while (cursor < text.size()) {
|
||||
size_t remaining = text.size() - cursor;
|
||||
size_t chunk_end = text.size();
|
||||
if (remaining > static_cast<size_t>(wrap_columns)) {
|
||||
chunk_end = cursor + static_cast<size_t>(wrap_columns);
|
||||
size_t break_at = text.rfind(' ', chunk_end);
|
||||
if (break_at == std::string::npos || break_at < cursor + static_cast<size_t>(wrap_columns / 3))
|
||||
break_at = chunk_end;
|
||||
chunk_end = break_at;
|
||||
}
|
||||
const std::string chunk = text.substr(cursor, chunk_end - cursor);
|
||||
drawSyntaxText(draw, {start.x + left_gutter, start.y + scaled(2.0f, scale) + base_row_height * static_cast<float>(line_index)},
|
||||
chunk, language, syntax);
|
||||
cursor = chunk_end;
|
||||
while (cursor < text.size() && text[cursor] == ' ') ++cursor;
|
||||
++line_index;
|
||||
}
|
||||
}
|
||||
|
||||
void drawCodeLineNumber(int value, float x, float y, ImU32 color) {
|
||||
@@ -460,6 +623,32 @@ void drawCodeLineNumber(int value, float x, float y, ImU32 color) {
|
||||
const std::string text = std::to_string(value);
|
||||
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,
|
||||
@@ -468,6 +657,7 @@ void DiffViewer::open(RepositoryView& repository, GitManager& manager, const std
|
||||
commit_id_.clear();
|
||||
staged_ = staged;
|
||||
mode_ = Mode::diff;
|
||||
scroll_to_top_ = true;
|
||||
reload(repository, manager, notice);
|
||||
}
|
||||
|
||||
@@ -477,10 +667,12 @@ void DiffViewer::openCommit(RepositoryView& repository, GitManager& manager,
|
||||
commit_id_ = commit_id;
|
||||
staged_ = false;
|
||||
mode_ = Mode::diff;
|
||||
scroll_to_top_ = true;
|
||||
reload(repository, manager, notice);
|
||||
if (hunks_.empty()) {
|
||||
mode_ = Mode::file;
|
||||
loadSupplement(repository, manager, mode_, notice);
|
||||
scroll_to_top_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,7 +683,7 @@ void DiffViewer::close() {
|
||||
hunks_.clear();
|
||||
file_lines_.clear();
|
||||
blame_lines_.clear();
|
||||
history_lines_.clear();
|
||||
history_entries_.clear();
|
||||
}
|
||||
|
||||
void DiffViewer::parseDiff(const std::string& text) {
|
||||
@@ -583,6 +775,30 @@ void DiffViewer::parseBlame(const std::string& text) {
|
||||
}
|
||||
}
|
||||
|
||||
void DiffViewer::parseHistory(const std::string& text) {
|
||||
history_entries_.clear();
|
||||
for (const std::string& line : splitLines(text)) {
|
||||
if (line.empty()) continue;
|
||||
HistoryEntry entry;
|
||||
size_t cursor = 0;
|
||||
auto consume_field = [&](std::string& out) {
|
||||
const size_t separator = line.find('\x1f', cursor);
|
||||
if (separator == std::string::npos) {
|
||||
out = line.substr(cursor);
|
||||
cursor = line.size();
|
||||
return;
|
||||
}
|
||||
out = line.substr(cursor, separator - cursor);
|
||||
cursor = separator + 1;
|
||||
};
|
||||
consume_field(entry.hash);
|
||||
consume_field(entry.date);
|
||||
consume_field(entry.author);
|
||||
consume_field(entry.summary);
|
||||
if (!entry.hash.empty() || !entry.summary.empty()) history_entries_.push_back(std::move(entry));
|
||||
}
|
||||
}
|
||||
|
||||
void DiffViewer::reload(RepositoryView& repository, GitManager& manager, std::string& notice) {
|
||||
std::vector<std::string> arguments;
|
||||
if (commit_id_.empty()) {
|
||||
@@ -620,7 +836,7 @@ void DiffViewer::reload(RepositoryView& repository, GitManager& manager, std::st
|
||||
}
|
||||
file_lines_.clear();
|
||||
blame_lines_.clear();
|
||||
history_lines_.clear();
|
||||
history_entries_.clear();
|
||||
if (mode_ != Mode::diff) loadSupplement(repository, manager, mode_, notice);
|
||||
}
|
||||
|
||||
@@ -649,108 +865,122 @@ void DiffViewer::loadSupplement(RepositoryView& repository, GitManager& manager,
|
||||
parseBlame(output);
|
||||
} else if (mode == Mode::history) {
|
||||
std::vector<std::string> arguments{
|
||||
"log", "--follow", "--date=short", "--pretty=format:%h %ad %an %s"};
|
||||
"log", "--follow", "--date=short", "--pretty=format:%h%x1f%ad%x1f%an%x1f%s"};
|
||||
if (!commit_id_.empty()) arguments.push_back(commit_id_);
|
||||
arguments.insert(arguments.end(), {"--", path_});
|
||||
if (!manager.captureGit(repository, arguments, output, error)) notice = error;
|
||||
history_lines_ = splitLines(output);
|
||||
parseHistory(output);
|
||||
}
|
||||
}
|
||||
|
||||
void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCache* avatars,
|
||||
float scale, ImFont* code_font, std::string& notice) {
|
||||
(void)avatars;
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {scaled(8, scale), scaled(5, scale)});
|
||||
ImGui::BeginChild("diff_viewer", {-1, -1}, ImGuiChildFlags_None,
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
ImGui::TextColored(ImVec4(0.94f, 0.66f, 0.25f, 1), ICON_TB_PEN);
|
||||
ImGui::SameLine(0, scaled(7, scale));
|
||||
ImGui::TextUnformatted(path_.c_str());
|
||||
|
||||
ImGui::SameLine(std::max(scaled(240, scale), ImGui::GetWindowWidth() - scaled(455, scale)));
|
||||
const bool historical = !commit_id_.empty();
|
||||
const std::string source_label = historical ? "Commit " + commit_id_.substr(0, 8)
|
||||
: staged_ ? "Staged" : "Unstaged";
|
||||
if (compactButton(source_label.c_str(), true)) {}
|
||||
ImGui::SameLine();
|
||||
if (compactButton("File View", mode_ == Mode::file)) {
|
||||
mode_ = Mode::file; loadSupplement(repository, manager, mode_, notice);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (compactButton("Diff View", mode_ == Mode::diff)) mode_ = Mode::diff;
|
||||
ImGui::SameLine(0, scaled(28, scale));
|
||||
if (compactButton("Blame", mode_ == Mode::blame)) {
|
||||
mode_ = Mode::blame; loadSupplement(repository, manager, mode_, notice);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (compactButton("History", mode_ == Mode::history)) {
|
||||
mode_ = Mode::history; loadSupplement(repository, manager, mode_, notice);
|
||||
}
|
||||
if (!historical) {
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.43f, 0.90f, 0.51f, 1));
|
||||
if (compactButton(staged_ ? "Unstage File" : "Stage File")) {
|
||||
const bool changed = staged_ ? manager.unstageFile(repository, path_, notice)
|
||||
: manager.stageFile(repository, path_, notice);
|
||||
if (changed) { staged_ = !staged_; reload(repository, manager, notice); }
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
const float top_right_width = scaled(72.0f, scale);
|
||||
ImGui::SameLine(std::max(scaled(160.0f, scale), ImGui::GetWindowWidth() - top_right_width));
|
||||
ImGui::TextDisabled("UTF-8");
|
||||
ImGui::SameLine(0, scaled(10.0f, scale));
|
||||
if (compactButton(ICON_TB_XMARK)) close();
|
||||
ImGui::Separator();
|
||||
if (!path_.empty()) {
|
||||
ImGui::SetCursorPosX(ImGui::GetWindowWidth() - scaled(116, scale));
|
||||
ImGui::TextDisabled("UTF-8");
|
||||
if (!historical && !staged_) {
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.94f, 0.44f, 0.44f, 1));
|
||||
if (compactButton(ICON_TB_TRASH_CAN)) {
|
||||
if (manager.discardFile(repository, path_, notice)) close();
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
const bool show_minimap = mode_ == Mode::diff || mode_ == Mode::file;
|
||||
const float minimap_width = scaled(56.0f, scale);
|
||||
const float segment_width = scaled(86.0f, scale);
|
||||
const float file_group_width = segment_width * 2.0f;
|
||||
const float blame_group_width = segment_width * 2.0f;
|
||||
const float wrap_width = scaled(34.0f, scale);
|
||||
const float between_groups = scaled(22.0f, scale);
|
||||
const float wrap_gap = scaled(12.0f, scale);
|
||||
const float toolbar_width = file_group_width + between_groups + blame_group_width + wrap_gap + wrap_width;
|
||||
ImGui::SetCursorPosX(std::max(0.0f, (ImGui::GetWindowWidth() - toolbar_width) * 0.5f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {0.0f, ImGui::GetStyle().ItemSpacing.y});
|
||||
if (toolbarSegmentButton("File View", mode_ == Mode::file, segment_width)) {
|
||||
mode_ = Mode::file;
|
||||
loadSupplement(repository, manager, mode_, notice);
|
||||
scroll_to_top_ = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (toolbarSegmentButton("Diff View", mode_ == Mode::diff, segment_width)) {
|
||||
mode_ = Mode::diff;
|
||||
scroll_to_top_ = true;
|
||||
}
|
||||
ImGui::SameLine(0, between_groups);
|
||||
if (toolbarSegmentButton("Blame", mode_ == Mode::blame, segment_width)) {
|
||||
mode_ = Mode::blame;
|
||||
loadSupplement(repository, manager, mode_, notice);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (toolbarSegmentButton("History", mode_ == Mode::history, segment_width)) {
|
||||
mode_ = Mode::history;
|
||||
loadSupplement(repository, manager, mode_, notice);
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::SameLine(0, wrap_gap);
|
||||
toolbarSegmentButton(ICON_TB_BARS, line_wrap_, wrap_width);
|
||||
if (ImGui::IsItemClicked()) line_wrap_ = !line_wrap_;
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort))
|
||||
ImGui::SetTooltip("%s", line_wrap_ ? "Disable line wrap" : "Enable line wrap");
|
||||
ImGui::Separator();
|
||||
|
||||
const bool show_minimap = mode_ == Mode::diff || mode_ == Mode::file || mode_ == Mode::blame || mode_ == Mode::history;
|
||||
const float minimap_width = scaled(168.0f, scale);
|
||||
const float minimap_gap = scaled(10.0f, scale);
|
||||
const float scrollbar_width = ImGui::GetStyle().ScrollbarSize;
|
||||
const SyntaxLanguage language = languageForPath(path_);
|
||||
float main_scroll_y = 0.0f;
|
||||
float main_window_height = 0.0f;
|
||||
float rendered_content_height = 0.0f;
|
||||
std::vector<MinimapEntry> minimap_entries;
|
||||
if (show_minimap) {
|
||||
minimap_entries.push_back({0, IM_COL32(0, 0, 0, 0)});
|
||||
minimap_entries.push_back({{{0.10f, IM_COL32(0, 0, 0, 0)}}, 0.65f});
|
||||
if (mode_ == Mode::diff) {
|
||||
for (size_t hunk_index = 0; hunk_index < hunks_.size(); ++hunk_index) {
|
||||
if (hunk_index > 0) {
|
||||
minimap_entries.push_back({0, IM_COL32(0, 0, 0, 0)});
|
||||
minimap_entries.push_back({0, IM_COL32(0, 0, 0, 0)});
|
||||
minimap_entries.push_back({{{0.10f, IM_COL32(0, 0, 0, 0)}}, 0.55f});
|
||||
minimap_entries.push_back({{{0.10f, IM_COL32(0, 0, 0, 0)}}, 0.45f});
|
||||
}
|
||||
minimap_entries.push_back({hunks_[hunk_index].header.size(), IM_COL32(128, 133, 141, 255)});
|
||||
for (const Line& line : hunks_[hunk_index].lines) {
|
||||
const ImU32 color = line.kind == LineKind::added ? IM_COL32(87, 190, 112, 255) :
|
||||
line.kind == LineKind::removed ? IM_COL32(220, 97, 97, 255) :
|
||||
IM_COL32(112, 118, 128, 255);
|
||||
minimap_entries.push_back({line.text.size() + 1, color});
|
||||
minimap_entries.push_back({buildMinimapSegments(line.text, language),
|
||||
line.kind == LineKind::context ? 1.0f : 1.0f});
|
||||
}
|
||||
minimap_entries.push_back({0, IM_COL32(0, 0, 0, 0)});
|
||||
minimap_entries.push_back({{{0.10f, IM_COL32(0, 0, 0, 0)}}, 0.30f});
|
||||
}
|
||||
} else {
|
||||
} else if (mode_ == Mode::file) {
|
||||
minimap_entries.reserve(file_lines_.size());
|
||||
for (const std::string& line : file_lines_)
|
||||
minimap_entries.push_back({line.size(), IM_COL32(112, 118, 128, 255)});
|
||||
minimap_entries.push_back({buildMinimapSegments(line, language), 1.0f});
|
||||
} else if (mode_ == Mode::blame) {
|
||||
minimap_entries.reserve(blame_lines_.size());
|
||||
for (const BlameLine& line : blame_lines_)
|
||||
minimap_entries.push_back({buildMinimapSegments(line.text, language), line.show_attribution ? 1.35f : 1.0f});
|
||||
} else {
|
||||
minimap_entries.reserve(history_entries_.size());
|
||||
for (const HistoryEntry& entry : history_entries_)
|
||||
minimap_entries.push_back({buildMinimapSegments(entry.summary, SyntaxLanguage::plain), 1.7f});
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::BeginChild("diff_content_main",
|
||||
show_minimap ? ImVec2{-minimap_width - scaled(6.0f, scale), -1} : ImVec2{-1, -1},
|
||||
ImGuiChildFlags_None, ImGuiWindowFlags_HorizontalScrollbar);
|
||||
ImGuiWindowFlags content_flags = line_wrap_ ? ImGuiWindowFlags_None : ImGuiWindowFlags_HorizontalScrollbar;
|
||||
if (scroll_to_top_) ImGui::SetNextWindowScroll({0.0f, 0.0f});
|
||||
ImGui::BeginChild("diff_content_main", ImVec2{-1, -1}, ImGuiChildFlags_None, content_flags);
|
||||
scroll_to_top_ = false;
|
||||
const bool use_code_font = code_font && mode_ != Mode::history;
|
||||
if (use_code_font) ImGui::PushFont(code_font, 0.0f);
|
||||
const float row_height = scaled(21, scale);
|
||||
const float content_width = std::max(ImGui::GetContentRegionAvail().x, scaled(200.0f, scale));
|
||||
const SyntaxLanguage language = languageForPath(path_);
|
||||
const ImVec2 child_minimum = ImGui::GetWindowPos();
|
||||
const ImVec2 child_size = ImGui::GetWindowSize();
|
||||
const float content_width = std::max(
|
||||
ImGui::GetContentRegionAvail().x - (show_minimap ? minimap_width + minimap_gap + scrollbar_width + scaled(6.0f, scale) : 0.0f),
|
||||
scaled(200.0f, scale));
|
||||
const float wrapable_text_width = std::max(scaled(32.0f, scale), content_width - scaled(12.0f, scale));
|
||||
const float code_character_width = ImGui::CalcTextSize("M").x;
|
||||
const int wrap_columns = line_wrap_ && code_character_width > 0.0f
|
||||
? std::max(12, static_cast<int>(wrapable_text_width / code_character_width))
|
||||
: 0;
|
||||
|
||||
// Keep the first source row clear of the toolbar and aligned with a normal row inset.
|
||||
ImGui::Dummy({0.0f, row_height});
|
||||
@@ -803,7 +1033,7 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCac
|
||||
line.kind == LineKind::removed ? IM_COL32(86, 38, 42, 170) : IM_COL32(0, 0, 0, 0);
|
||||
drawCodeLine(line.text, language,
|
||||
line.kind == LineKind::removed ? old_syntax : new_syntax,
|
||||
scale, background, code_gutter, content_width);
|
||||
scale, background, code_gutter, content_width, 0.0f, wrap_columns);
|
||||
const float text_y = line_minimum.y + scaled(2.0f, scale);
|
||||
drawCodeLineNumber(line.old_number, line_minimum.x + scaled(6.0f, scale), text_y,
|
||||
IM_COL32(126, 132, 142, 255));
|
||||
@@ -816,28 +1046,67 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCac
|
||||
if (pending_hunk >= 0 && pending_hunk < static_cast<int>(hunks_.size())) {
|
||||
const bool cached = pending_action != HunkAction::discard;
|
||||
const bool reverse = pending_action != HunkAction::stage;
|
||||
if (manager.applyPatch(repository, hunks_[pending_hunk].patch, cached, reverse, notice))
|
||||
if (manager.applyPatch(repository, hunks_[pending_hunk].patch, cached, reverse, notice)) {
|
||||
if (pending_action == HunkAction::stage) close();
|
||||
reload(repository, manager, notice);
|
||||
}
|
||||
}
|
||||
} else if (mode_ == Mode::blame) {
|
||||
std::ostringstream blame_text;
|
||||
for (size_t index = 0; index < blame_lines_.size(); ++index) {
|
||||
const BlameLine& line = blame_lines_[index];
|
||||
if (index) blame_text << '\n';
|
||||
blame_text << line.line_number << " ";
|
||||
if (line.show_attribution) {
|
||||
blame_text << line.summary;
|
||||
if (!line.date.empty()) blame_text << " " << line.date;
|
||||
blame_text << " ";
|
||||
} else {
|
||||
blame_text << "| ";
|
||||
if (blame_lines_.empty()) {
|
||||
ImGui::TextDisabled("No blame data is available for this file.");
|
||||
} else {
|
||||
SyntaxState syntax;
|
||||
const float info_width = scaled(270.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) {
|
||||
ImGui::PushID(static_cast<int>(index));
|
||||
const BlameLine& line = blame_lines_[index];
|
||||
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);
|
||||
drawCodeLine(line.text, language, syntax, scale, background, code_gutter, content_width,
|
||||
blame_row_height, wrap_columns);
|
||||
ImDrawList* draw = ImGui::GetWindowDrawList();
|
||||
const ImVec2 line_maximum = ImGui::GetItemRectMax();
|
||||
draw->AddRectFilled({line_minimum.x, line_minimum.y},
|
||||
{line_minimum.x + scaled(3.0f, scale), line_maximum.y}, accent);
|
||||
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 unsigned int avatar_texture = avatars ? avatars->textureFor(line.email) : 0;
|
||||
if (avatar_texture) {
|
||||
draw->AddImageRounded(ImTextureRef(static_cast<ImTextureID>(avatar_texture)),
|
||||
avatar_min, {avatar_min.x + avatar_size, avatar_min.y + avatar_size},
|
||||
{0, 0}, {1, 1}, IM_COL32_WHITE, scaled(3.0f, scale));
|
||||
} else {
|
||||
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);
|
||||
} else {
|
||||
draw->AddText({line_minimum.x + scaled(16.0f, scale), line_minimum.y + scaled(2.0f, scale)},
|
||||
IM_COL32(92, 99, 110, 255), "...");
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
blame_text << line.text;
|
||||
}
|
||||
drawSelectableTextBlock("##blame_text", blame_text.str(), {-1, -1});
|
||||
if (blame_lines_.empty()) ImGui::TextDisabled("No blame data is available for this file.");
|
||||
} else {
|
||||
const std::vector<std::string>* lines = mode_ == Mode::file ? &file_lines_ : &history_lines_;
|
||||
} else if (mode_ == Mode::file) {
|
||||
const std::vector<std::string>* lines = &file_lines_;
|
||||
if (mode_ == Mode::file) {
|
||||
SyntaxState syntax;
|
||||
const float code_gutter = scaled(52.0f, scale);
|
||||
@@ -845,31 +1114,63 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCac
|
||||
ImGui::PushID(static_cast<int>(index));
|
||||
const ImVec2 line_minimum = ImGui::GetCursorScreenPos();
|
||||
drawCodeLine((*lines)[index], language, syntax, scale, IM_COL32(0, 0, 0, 0),
|
||||
code_gutter, content_width);
|
||||
code_gutter, content_width, 0.0f, wrap_columns);
|
||||
drawCodeLineNumber(static_cast<int>(index + 1), line_minimum.x + scaled(6.0f, scale),
|
||||
line_minimum.y + scaled(2.0f, scale), IM_COL32(126, 132, 142, 255));
|
||||
ImGui::PopID();
|
||||
}
|
||||
} else {
|
||||
drawSelectableTextBlock("##history_text", joinLines(*lines), {-1, -1});
|
||||
}
|
||||
if (lines->empty()) ImGui::TextDisabled("No data is available for this view.");
|
||||
} else {
|
||||
if (history_entries_.empty()) {
|
||||
ImGui::TextDisabled("No history data is available for this file.");
|
||||
} else {
|
||||
for (size_t index = 0; index < history_entries_.size(); ++index) {
|
||||
ImGui::PushID(static_cast<int>(index));
|
||||
const HistoryEntry& entry = history_entries_[index];
|
||||
const ImVec2 row_min = ImGui::GetCursorScreenPos();
|
||||
const float history_row_height = scaled(36.0f, scale);
|
||||
ImGui::InvisibleButton("##history_row", {content_width, history_row_height});
|
||||
const ImVec2 row_max = ImGui::GetItemRectMax();
|
||||
ImDrawList* draw = ImGui::GetWindowDrawList();
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
draw->AddRectFilled(row_min, row_max,
|
||||
hovered ? IM_COL32(49, 53, 61, 180) : IM_COL32(37, 40, 47, 130), scaled(4.0f, scale));
|
||||
draw->AddRectFilled(row_min, {row_min.x + scaled(3.0f, scale), row_max.y},
|
||||
IM_COL32(23, 181, 204, 220), scaled(3.0f, scale));
|
||||
|
||||
const std::string hash_text = entry.hash.empty() ? "-" : entry.hash;
|
||||
draw->AddText({row_min.x + scaled(10.0f, scale), row_min.y + scaled(4.0f, scale)},
|
||||
IM_COL32(96, 184, 235, 255), hash_text.c_str());
|
||||
|
||||
const std::string author_date = entry.author +
|
||||
(entry.date.empty() ? std::string{} : std::string(" ") + entry.date);
|
||||
draw->AddText({row_min.x + scaled(78.0f, scale), row_min.y + scaled(4.0f, scale)},
|
||||
IM_COL32(188, 192, 198, 255), author_date.c_str());
|
||||
|
||||
const std::string summary = entry.summary.empty() ? "(no summary)" : entry.summary;
|
||||
draw->AddText({row_min.x + scaled(10.0f, scale), row_min.y + scaled(18.0f, scale)},
|
||||
IM_COL32(219, 223, 229, 255), summary.c_str());
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
}
|
||||
main_scroll_y = ImGui::GetScrollY();
|
||||
main_window_height = ImGui::GetWindowHeight();
|
||||
rendered_content_height = ImGui::GetCursorPosY();
|
||||
const float max_scroll_y = ImGui::GetScrollMaxY();
|
||||
if (show_minimap) {
|
||||
const float minimap_height = std::max(scaled(40.0f, scale), child_size.y - scaled(10.0f, scale));
|
||||
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);
|
||||
ImGui::SetCursorScreenPos({minimap_x, minimap_y});
|
||||
ImGui::PushClipRect(child_minimum, {child_minimum.x + child_size.x, child_minimum.y + child_size.y}, true);
|
||||
drawMinimap(minimap_entries, {minimap_width, minimap_height}, scale,
|
||||
rendered_content_height, main_scroll_y, main_window_height, max_scroll_y);
|
||||
ImGui::PopClipRect();
|
||||
}
|
||||
if (use_code_font) ImGui::PopFont();
|
||||
ImGui::EndChild();
|
||||
if (show_minimap) {
|
||||
ImGui::SameLine(0, scaled(6.0f, scale));
|
||||
const float minimap_line_height = scaled(2.0f, scale);
|
||||
const float minimap_height = std::min(main_window_height,
|
||||
std::max(scaled(36.0f, scale), static_cast<float>(minimap_entries.size()) * minimap_line_height + scaled(8.0f, scale)));
|
||||
ImGui::BeginChild("diff_content_minimap", {minimap_width, minimap_height}, ImGuiChildFlags_None,
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
drawMinimap(minimap_entries, scale, rendered_content_height, main_scroll_y, main_window_height);
|
||||
ImGui::EndChild();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
|
||||
@@ -44,6 +44,12 @@ private:
|
||||
int line_number = 0;
|
||||
bool show_attribution = false;
|
||||
};
|
||||
struct HistoryEntry {
|
||||
std::string hash;
|
||||
std::string date;
|
||||
std::string author;
|
||||
std::string summary;
|
||||
};
|
||||
|
||||
std::string path_;
|
||||
std::string commit_id_;
|
||||
@@ -53,10 +59,13 @@ private:
|
||||
std::vector<Hunk> hunks_;
|
||||
std::vector<std::string> file_lines_;
|
||||
std::vector<BlameLine> blame_lines_;
|
||||
std::vector<std::string> history_lines_;
|
||||
std::vector<HistoryEntry> history_entries_;
|
||||
bool line_wrap_ = false;
|
||||
bool scroll_to_top_ = false;
|
||||
|
||||
void reload(RepositoryView& repository, GitManager& manager, std::string& notice);
|
||||
void loadSupplement(RepositoryView& repository, GitManager& manager, Mode mode, std::string& notice);
|
||||
void parseDiff(const std::string& text);
|
||||
void parseBlame(const std::string& text);
|
||||
void parseHistory(const std::string& text);
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,16 @@
|
||||
|
||||
namespace {
|
||||
|
||||
struct RoutedEdge {
|
||||
int child_row = -1;
|
||||
int parent_row = -1;
|
||||
int child_lane = 0;
|
||||
int parent_lane = 0;
|
||||
int route_slot = 0;
|
||||
int color_lane = 0;
|
||||
int detour_lane = -1;
|
||||
};
|
||||
|
||||
template <size_t Count>
|
||||
void drawRoundedPolyline(ImDrawList* draw, const std::array<ImVec2, Count>& points,
|
||||
float radius, ImU32 color, float thickness) {
|
||||
@@ -164,64 +174,99 @@ void GraphRenderer::drawRow(int row, const CommitInfo& commit,
|
||||
(row_heights[static_cast<size_t>(index)] - ImGui::GetStyle().CellPadding.y * 2.0f) * 0.5f;
|
||||
};
|
||||
|
||||
int maximum_lane = 0;
|
||||
for (const auto& item : commits) maximum_lane = std::max(maximum_lane, item.lane);
|
||||
|
||||
std::vector<RoutedEdge> routed_edges;
|
||||
routed_edges.reserve(commits.size() * 2);
|
||||
const auto route_spans_overlap = [](const RoutedEdge& left, const RoutedEdge& right) {
|
||||
const int top = std::max(left.child_row, right.child_row);
|
||||
const int bottom = std::min(left.parent_row, right.parent_row);
|
||||
return top < bottom;
|
||||
};
|
||||
const auto commit_blocks_lane = [&](int candidate_lane, int child_row, int parent_row) {
|
||||
for (int intermediate = child_row + 1; intermediate < parent_row; ++intermediate) {
|
||||
if (row_heights[static_cast<size_t>(intermediate)] > 0.0f &&
|
||||
commits[static_cast<size_t>(intermediate)].lane == candidate_lane)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const auto route_blocks_lane = [&](int candidate_lane, int child_row, int parent_row) {
|
||||
const RoutedEdge probe{child_row, parent_row, 0, 0, 0, 0, candidate_lane};
|
||||
return std::any_of(routed_edges.begin(), routed_edges.end(), [&](const RoutedEdge& edge) {
|
||||
return edge.detour_lane == candidate_lane && route_spans_overlap(edge, probe);
|
||||
});
|
||||
};
|
||||
const auto lane_available = [&](int candidate_lane, int child_row, int parent_row) {
|
||||
return !commit_blocks_lane(candidate_lane, child_row, parent_row) &&
|
||||
!route_blocks_lane(candidate_lane, child_row, parent_row);
|
||||
};
|
||||
|
||||
for (int child_row = 0; child_row < static_cast<int>(commits.size()); ++child_row) {
|
||||
const auto& child_parents = parent_rows[static_cast<size_t>(child_row)];
|
||||
for (size_t parent_index = 0; parent_index < child_parents.size(); ++parent_index) {
|
||||
const int parent_row = child_parents[parent_index];
|
||||
if (parent_row <= child_row) continue;
|
||||
|
||||
const CommitInfo& child_commit = commits[static_cast<size_t>(child_row)];
|
||||
const CommitInfo& parent_commit = commits[static_cast<size_t>(parent_row)];
|
||||
const int preferred_lane = parent_index == 0 ? child_commit.lane : parent_commit.lane;
|
||||
int detour_lane = -1;
|
||||
|
||||
if (!lane_available(preferred_lane, child_row, parent_row)) {
|
||||
for (int distance = 1; distance <= maximum_lane + 2; ++distance) {
|
||||
const int left = preferred_lane - distance;
|
||||
const int right = preferred_lane + distance;
|
||||
if (left >= 0 && lane_available(left, child_row, parent_row)) {
|
||||
detour_lane = left;
|
||||
break;
|
||||
}
|
||||
if (lane_available(right, child_row, parent_row)) {
|
||||
detour_lane = right;
|
||||
maximum_lane = std::max(maximum_lane, right);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (detour_lane < 0) {
|
||||
detour_lane = maximum_lane + 1;
|
||||
maximum_lane = detour_lane;
|
||||
}
|
||||
}
|
||||
|
||||
RoutedEdge route;
|
||||
route.child_row = child_row;
|
||||
route.parent_row = parent_row;
|
||||
route.child_lane = child_commit.lane;
|
||||
route.parent_lane = parent_commit.lane;
|
||||
route.route_slot = static_cast<int>(parent_index);
|
||||
route.detour_lane = detour_lane;
|
||||
route.color_lane = detour_lane >= 0 ? detour_lane : preferred_lane;
|
||||
routed_edges.push_back(route);
|
||||
}
|
||||
}
|
||||
|
||||
// Every row redraws edges crossing its clip rectangle. This keeps long paths continuous
|
||||
// without allowing table row clipping to cut out intermediate segments.
|
||||
draw->PushClipRect(
|
||||
{origin.x, origin.y - row_clip_padding},
|
||||
{origin.x + ImGui::GetContentRegionAvail().x, origin.y + content_height + row_clip_padding}, true);
|
||||
for (int child_row = 0; child_row < static_cast<int>(commits.size()); ++child_row) {
|
||||
if (row_heights[static_cast<size_t>(child_row)] <= 0.0f) continue;
|
||||
const CommitInfo& child = commits[static_cast<size_t>(child_row)];
|
||||
const auto& child_parents = parent_rows[static_cast<size_t>(child_row)];
|
||||
for (size_t parent_index = 0; parent_index < child_parents.size(); ++parent_index) {
|
||||
const int parent_row = child_parents[parent_index];
|
||||
if (parent_row <= child_row || row < child_row || row > parent_row ||
|
||||
row_heights[static_cast<size_t>(parent_row)] <= 0.0f) continue;
|
||||
const CommitInfo& parent = commits[static_cast<size_t>(parent_row)];
|
||||
for (const RoutedEdge& route : routed_edges) {
|
||||
if (row < route.child_row || row > route.parent_row ||
|
||||
row_heights[static_cast<size_t>(route.child_row)] <= 0.0f ||
|
||||
row_heights[static_cast<size_t>(route.parent_row)] <= 0.0f)
|
||||
continue;
|
||||
|
||||
const float child_x = origin.x + px(17.0f) + lane_spacing * route.child_lane;
|
||||
const float parent_x = origin.x + px(17.0f) + lane_spacing * route.parent_lane;
|
||||
const float child_y = center_y(route.child_row);
|
||||
const float parent_y = center_y(route.parent_row);
|
||||
const float detour_x = route.detour_lane >= 0
|
||||
? origin.x + px(17.0f) + lane_spacing * route.detour_lane
|
||||
: std::numeric_limits<float>::quiet_NaN();
|
||||
|
||||
const float child_x = origin.x + px(17.0f) + lane_spacing * child.lane;
|
||||
const float parent_x = origin.x + px(17.0f) + lane_spacing * parent.lane;
|
||||
const float child_y = center_y(child_row);
|
||||
const float parent_y = center_y(parent_row);
|
||||
const int preferred_lane = parent_index == 0 ? child.lane : parent.lane;
|
||||
const auto lane_is_blocked = [&](int candidate_lane) {
|
||||
for (int intermediate = child_row + 1; intermediate < parent_row; ++intermediate) {
|
||||
if (row_heights[static_cast<size_t>(intermediate)] > 0.0f &&
|
||||
commits[static_cast<size_t>(intermediate)].lane == candidate_lane)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
float detour_x = std::numeric_limits<float>::quiet_NaN();
|
||||
if (lane_is_blocked(preferred_lane)) {
|
||||
int maximum_lane = 0;
|
||||
for (int intermediate = child_row; intermediate <= parent_row; ++intermediate) {
|
||||
if (row_heights[static_cast<size_t>(intermediate)] > 0.0f)
|
||||
maximum_lane = std::max(maximum_lane,
|
||||
commits[static_cast<size_t>(intermediate)].lane);
|
||||
}
|
||||
int detour_lane = maximum_lane + 1;
|
||||
for (int distance = 1; distance <= maximum_lane + 1; ++distance) {
|
||||
const int left = preferred_lane - distance;
|
||||
const int right = preferred_lane + distance;
|
||||
if (left >= 0 && !lane_is_blocked(left)) {
|
||||
detour_lane = left;
|
||||
break;
|
||||
}
|
||||
if (right <= maximum_lane && !lane_is_blocked(right)) {
|
||||
detour_lane = right;
|
||||
break;
|
||||
}
|
||||
}
|
||||
detour_x = origin.x + px(17.0f) + lane_spacing * detour_lane;
|
||||
}
|
||||
// Colors are lane-position based, so edges use the visible lane slot they route through.
|
||||
const int edge_color = std::isfinite(detour_x)
|
||||
? static_cast<int>(std::lround((detour_x - (origin.x + px(17.0f))) / lane_spacing))
|
||||
: preferred_lane;
|
||||
drawOrthogonalEdge(draw, {child_x, child_y}, {parent_x, parent_y},
|
||||
laneColor(edge_color, 235), scale_, static_cast<int>(parent_index), detour_x);
|
||||
}
|
||||
laneColor(route.color_lane, 235), scale_, route.route_slot, detour_x);
|
||||
}
|
||||
draw->PopClipRect();
|
||||
|
||||
|
||||
17
vendor/icons/IconsTabler.h
vendored
17
vendor/icons/IconsTabler.h
vendored
@@ -12,6 +12,17 @@
|
||||
#define ICON_TB_ARROW_UP "\xee\xa8\xa5"
|
||||
#define ICON_TB_ARROW_UP_RIGHT_FROM_SQUARE "\xee\xaa\x99"
|
||||
#define ICON_TB_BARS "\xee\xb1\x82"
|
||||
#define ICON_TB_BRAND_C_SHARP "\xef\x80\x83"
|
||||
#define ICON_TB_BRAND_CPP "\xef\x97\xbe"
|
||||
#define ICON_TB_BRAND_CSS3 "\xee\xb5\xab"
|
||||
#define ICON_TB_BRAND_GOLANG "\xef\x9e\x8d"
|
||||
#define ICON_TB_BRAND_HTML5 "\xee\xb5\xac"
|
||||
#define ICON_TB_BRAND_JAVASCRIPT "\xee\xbc\x8c"
|
||||
#define ICON_TB_BRAND_NODEJS "\xef\xab\xa0"
|
||||
#define ICON_TB_BRAND_POWERSHELL "\xef\x97\xad"
|
||||
#define ICON_TB_BRAND_PYTHON "\xee\xb4\x81"
|
||||
#define ICON_TB_BRAND_RUST "\xef\xa9\x93"
|
||||
#define ICON_TB_BRAND_TYPESCRIPT "\xef\x97\xb1"
|
||||
#define ICON_TB_BOX_ARCHIVE "\xee\xa8\x8b"
|
||||
#define ICON_TB_BOX_OPEN "\xef\x81\xba"
|
||||
#define ICON_TB_CARET_DOWN "\xee\xad\x9d"
|
||||
@@ -34,13 +45,17 @@
|
||||
#define ICON_TB_DOWNLOAD "\xee\xaa\x96"
|
||||
#define ICON_TB_EYE "\xee\xaa\x9a"
|
||||
#define ICON_TB_FILE "\xee\xaa\xa4"
|
||||
#define ICON_TB_FILE_CODE "\xee\xaf\x90"
|
||||
#define ICON_TB_FILE_TEXT "\xee\xaa\xa2"
|
||||
#define ICON_TB_FOLDER "\xee\xaa\xad"
|
||||
#define ICON_TB_FOLDER_OPEN "\xef\xab\xb7"
|
||||
#define ICON_TB_FOLDER_TREE "\xee\xba\x9d"
|
||||
#define ICON_TB_GLOBE "\xee\xad\x94"
|
||||
#define ICON_TB_JET_FIGHTER_UP "\xef\x87\xad"
|
||||
#define ICON_TB_JSON "\xef\x9e\xb2"
|
||||
#define ICON_TB_LAYERS_LINKED "\xee\xba\xa1"
|
||||
#define ICON_TB_MAGNIFYING_GLASS "\xee\xac\x9c"
|
||||
#define ICON_TB_MARKDOWN "\xee\xb1\x81"
|
||||
#define ICON_TB_MINUS "\xee\xab\xb2"
|
||||
#define ICON_TB_PEN "\xee\xac\x84"
|
||||
#define ICON_TB_PLUS "\xee\xac\x8b"
|
||||
@@ -50,6 +65,7 @@
|
||||
#define ICON_TB_SERVER "\xee\xac\x9f"
|
||||
#define ICON_TB_TAG "\xee\xbe\x86"
|
||||
#define ICON_TB_TERMINAL "\xee\xaf\xaf"
|
||||
#define ICON_TB_TERMINAL_2 "\xee\xaf\xaf"
|
||||
#define ICON_TB_TRASH_CAN "\xee\xad\x81"
|
||||
#define ICON_TB_TREE "\xee\xbc\x81"
|
||||
#define ICON_TB_TRIANGLE_EXCLAMATION "\xee\xa8\x86"
|
||||
@@ -57,3 +73,4 @@
|
||||
#define ICON_TB_USER "\xee\xad\x8d"
|
||||
#define ICON_TB_WINDOW_MAXIMIZE "\xee\xab\xaa"
|
||||
#define ICON_TB_XMARK "\xee\xad\x95"
|
||||
#define ICON_TB_FILE_TYPE_SQL "\xef\xac\x95"
|
||||
|
||||
2
vendor/libgit2
vendored
2
vendor/libgit2
vendored
Submodule vendor/libgit2 updated: 44c05e5d12...73951e920a
Reference in New Issue
Block a user