Compare commits
91 Commits
prod-8-513
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c9ff53c77b | |||
| bec13359a8 | |||
| 120965f507 | |||
| 3e4529204e | |||
| 6c245468b9 | |||
| b85c2b75b6 | |||
| f614199ce5 | |||
| 9424c7b830 | |||
| e4a8e5d8c9 | |||
| 0cc7f7151a | |||
| bf3a50c0e5 | |||
| a087fe429b | |||
| 139e721fcc | |||
| 96b595b27c | |||
| ca6968ae5e | |||
| d7fddcb728 | |||
| 6059945771 | |||
| 181736c0c8 | |||
| 0e08a8a190 | |||
| e68065a4e2 | |||
| 069d4e341f | |||
| ce81922ebb | |||
| ac9df86ef0 | |||
| 1f800c3cef | |||
| 2ca9c6bf77 | |||
| 7a39b4aa20 | |||
| 0fea9ab8a4 | |||
| d338023e59 | |||
| cb145fe722 | |||
| 6ecbd597b6 | |||
| 2c07bb0132 | |||
| 1114c05cb9 | |||
| 52ed46a203 | |||
| 9ebf3699ad | |||
| 258973da57 | |||
| 4b846a62cd | |||
| 4dab8e8a9a | |||
| 76634c3fd3 | |||
| 1c730302d5 | |||
| 93f67534c6 | |||
| 5ba7d46809 | |||
| f53efe92c4 | |||
| f7f95e28ae | |||
| 8d59d066a6 | |||
| aebfe65352 | |||
| b6fdec12f5 | |||
| 5ac621d7b5 | |||
| 6361002f53 | |||
| e74e6ec513 | |||
| 279fe6e7f9 | |||
| 79bd00d84d | |||
| eeb134c6ab | |||
| 1ac26e3e36 | |||
| b94ca105de | |||
| 60d1e67fbe | |||
| 5c9047b444 | |||
| 45a4ceb72a | |||
| 9a274c148e | |||
| f869ecf46b | |||
| b6400880e8 | |||
| fb98fe6106 | |||
| c99f0cc34a | |||
| 5c6cd4649f | |||
| 0b220a382e | |||
| 5a2cffc177 | |||
| 6214c97b28 | |||
| 2c7cabb0f9 | |||
| 2e38d51f74 | |||
| 755092933f | |||
| a5b0e02387 | |||
| ee7efa6eff | |||
| 303d8469a0 | |||
| 7989224a8c | |||
| 106a2b8cf9 | |||
| 8368b0e237 | |||
| 942e95e7a1 | |||
| 0425bb7320 | |||
| 508c0f037e | |||
| e1107756f9 | |||
| e3b6c34e88 | |||
| 57d7be3357 | |||
| 2d451a5ece | |||
| a64770b684 | |||
| 009a4efb87 | |||
| b01bbd2b73 | |||
| b100b19b16 | |||
| dff02adc45 | |||
| cdea4503c7 | |||
| a8bec3ed22 | |||
| fefc2084ac | |||
| 0152667b65 |
BIN
.gitea/images/gitree_banner.png
Normal file
BIN
.gitea/images/gitree_banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 782 KiB |
BIN
.gitea/images/gitree_banner_rounded.png
Normal file
BIN
.gitea/images/gitree_banner_rounded.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 820 KiB |
BIN
.gitea/images/gitree_logo.png
Normal file
BIN
.gitea/images/gitree_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 794 KiB |
BIN
.gitea/images/gitree_logo_no_bg.png
Normal file
BIN
.gitea/images/gitree_logo_no_bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 123 KiB |
BIN
.gitea/images/id-engine.jpg
Normal file
BIN
.gitea/images/id-engine.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@@ -1,12 +1,16 @@
|
|||||||
name: Build Windows EXE
|
name: Build Releases
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "**"
|
- "**"
|
||||||
|
|
||||||
|
env:
|
||||||
|
APP_NAME: gitree
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-windows:
|
build-windows:
|
||||||
|
name: Build Windows x64_86
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -14,6 +18,23 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Sync submodules
|
||||||
|
run: |
|
||||||
|
git submodule sync --recursive
|
||||||
|
git submodule update --init --force --recursive
|
||||||
|
git submodule status --recursive
|
||||||
|
|
||||||
|
- name: Generate build names
|
||||||
|
env:
|
||||||
|
GITEA_SHA: ${{ gitea.sha }}
|
||||||
|
run: |
|
||||||
|
short_sha="$(printf '%s' "$GITEA_SHA" | cut -c1-7)"
|
||||||
|
|
||||||
|
echo "BUILD_HASH=${short_sha}" >> "$GITHUB_ENV"
|
||||||
|
echo "WINDOWS_EXE=${APP_NAME}-windows-x64_86-${short_sha}.exe" >> "$GITHUB_ENV"
|
||||||
|
echo "WINDOWS_ARTIFACT=${APP_NAME}-windows-x64_86-${short_sha}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -25,10 +46,13 @@ jobs:
|
|||||||
cat > mingw-toolchain.cmake <<'EOF'
|
cat > mingw-toolchain.cmake <<'EOF'
|
||||||
set(CMAKE_SYSTEM_NAME Windows)
|
set(CMAKE_SYSTEM_NAME Windows)
|
||||||
set(CMAKE_SYSTEM_PROCESSOR x86_64)
|
set(CMAKE_SYSTEM_PROCESSOR x86_64)
|
||||||
|
|
||||||
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
|
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
|
||||||
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
|
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
|
||||||
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
|
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
|
||||||
|
|
||||||
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
|
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
|
||||||
|
|
||||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||||
@@ -38,46 +62,339 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cmake -S . -B build-win -G Ninja \
|
cmake -S . -B build-win -G Ninja \
|
||||||
-DCMAKE_TOOLCHAIN_FILE=mingw-toolchain.cmake \
|
-DCMAKE_TOOLCHAIN_FILE=mingw-toolchain.cmake \
|
||||||
-DCMAKE_BUILD_TYPE=Release
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DBUILD_SHARED_LIBS=OFF \
|
||||||
|
-DCMAKE_EXE_LINKER_FLAGS="-static -static-libgcc -static-libstdc++"
|
||||||
|
|
||||||
- name: Build Windows executable
|
- name: Build Windows executable
|
||||||
run: cmake --build build-win --parallel
|
run: cmake --build build-win --parallel
|
||||||
|
|
||||||
- name: Prepare artifact
|
- 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 "$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
|
||||||
|
echo "The executable still depends on a MinGW runtime DLL."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Prepare Windows artifact
|
||||||
run: |
|
run: |
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
cp build-win/bin/gitree.exe dist/Gitree-windows-x64.exe
|
cp "$WINDOWS_EXE_PATH" "dist/${WINDOWS_EXE}"
|
||||||
|
|
||||||
- name: Upload Windows build
|
- name: Upload Windows build
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: Gitree-windows-x64
|
name: ${{ env.WINDOWS_ARTIFACT }}
|
||||||
path: dist/Gitree-windows-x64.exe
|
path: dist/${{ env.WINDOWS_EXE }}
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Create prod release
|
build-linux:
|
||||||
if: ${{ gitea.ref_name == 'prod' }}
|
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: Sync submodules
|
||||||
|
run: |
|
||||||
|
git submodule sync --recursive
|
||||||
|
git submodule update --init --force --recursive
|
||||||
|
git submodule status --recursive
|
||||||
|
|
||||||
|
- name: Generate build names
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
||||||
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
|
||||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
|
||||||
GITEA_SHA: ${{ gitea.sha }}
|
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 \
|
||||||
|
libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev \
|
||||||
|
libgl1-mesa-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: Sync submodules
|
||||||
|
run: |
|
||||||
|
git submodule sync --recursive
|
||||||
|
git submodule update --init --force --recursive
|
||||||
|
git submodule status --recursive
|
||||||
|
|
||||||
|
- name: Generate release names
|
||||||
|
run: |
|
||||||
|
short_sha="$(printf '%s' "$GITEA_SHA" | cut -c1-7)"
|
||||||
|
|
||||||
|
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_86-${short_sha}.exe" >> "$GITHUB_ENV"
|
||||||
|
echo "WINDOWS_ARTIFACT=${APP_NAME}-windows-x64_86-${short_sha}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
echo "LINUX_DEB=${APP_NAME}-deb-x64-${short_sha}.deb" >> "$GITHUB_ENV"
|
||||||
|
echo "LINUX_ARTIFACT=${APP_NAME}-deb-x64-${short_sha}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- 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: |
|
run: |
|
||||||
if [ -z "$GITEA_TOKEN" ]; then
|
if [ -z "$GITEA_TOKEN" ]; then
|
||||||
echo "The repository secret GITEA_TOKEN is required to publish prod releases."
|
echo "The repository secret GITEA_TOKEN is required to publish prod releases."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
short_sha="$(printf '%s' "$GITEA_SHA" | cut -c1-7)"
|
git fetch --tags --force
|
||||||
tag="prod-${GITEA_RUN_NUMBER}-${short_sha}"
|
|
||||||
api="${GITEA_SERVER_URL%/}/api/v1/repos/${GITEA_REPOSITORY}"
|
api="${GITEA_SERVER_URL%/}/api/v1/repos/${GITEA_REPOSITORY}"
|
||||||
|
repo_url="${GITEA_SERVER_URL%/}/${GITEA_REPOSITORY}"
|
||||||
|
|
||||||
|
previous_tag="$(
|
||||||
|
curl --fail-with-body --silent --show-error \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${api}/releases?limit=50" |
|
||||||
|
jq -r '[.[] | select(.tag_name | startswith("release-"))][0].tag_name // empty'
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [ -n "$previous_tag" ]; then
|
||||||
|
range="${previous_tag}..${GITEA_SHA}"
|
||||||
|
else
|
||||||
|
range="${GITEA_SHA}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
if [ -n "$previous_tag" ]; then
|
||||||
|
echo "## Changes since ${previous_tag}"
|
||||||
|
else
|
||||||
|
echo "## Changes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
|
||||||
|
commit_count="$(git rev-list --count "$range")"
|
||||||
|
|
||||||
|
if [ "$commit_count" -eq 0 ]; then
|
||||||
|
echo "- No commits found since the previous release."
|
||||||
|
else
|
||||||
|
git log "$range" \
|
||||||
|
--reverse \
|
||||||
|
--pretty=format:'%H%x1f%s' |
|
||||||
|
while IFS="$(printf '\037')" read -r hash subject; do
|
||||||
|
short="$(printf '%s' "$hash" | cut -c1-7)"
|
||||||
|
echo "- ([${short}](${repo_url}/commit/${hash})) ${subject}"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "## Builds"
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [ "$WINDOWS_RESULT" = "success" ]; then
|
||||||
|
echo "- ${WINDOWS_EXE}"
|
||||||
|
else
|
||||||
|
echo "- Windows x64_86 failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$LINUX_RESULT" = "success" ]; then
|
||||||
|
echo "- ${LINUX_DEB}"
|
||||||
|
else
|
||||||
|
echo "- Linux DEB x64 failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "## Authors"
|
||||||
|
echo
|
||||||
|
echo "Sorted by total lines added or removed."
|
||||||
|
echo
|
||||||
|
|
||||||
|
git log "$range" --numstat --format='author:%an <%ae>' |
|
||||||
|
awk '
|
||||||
|
/^author:/ {
|
||||||
|
author = substr($0, 8)
|
||||||
|
next
|
||||||
|
}
|
||||||
|
|
||||||
|
NF >= 3 {
|
||||||
|
added = ($1 == "-" ? 0 : $1)
|
||||||
|
removed = ($2 == "-" ? 0 : $2)
|
||||||
|
|
||||||
|
adds[author] += added
|
||||||
|
dels[author] += removed
|
||||||
|
churn[author] += added + removed
|
||||||
|
}
|
||||||
|
|
||||||
|
END {
|
||||||
|
for (a in churn) {
|
||||||
|
printf "%d\t%d\t%d\t%s\n", churn[a], adds[a], dels[a], a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' |
|
||||||
|
sort -nr |
|
||||||
|
while IFS="$(printf '\t')" read -r total added removed author; do
|
||||||
|
echo "- ${author} — ${total} lines changed (+${added} / -${removed})"
|
||||||
|
done
|
||||||
|
} > release-notes.md
|
||||||
|
|
||||||
payload="$(jq -n \
|
payload="$(jq -n \
|
||||||
--arg tag "$tag" \
|
--arg tag "$RELEASE_TAG" \
|
||||||
--arg sha "$GITEA_SHA" \
|
--arg sha "$GITEA_SHA" \
|
||||||
--arg name "Gitree ${tag}" \
|
--arg name "$RELEASE_NAME" \
|
||||||
'{tag_name: $tag, target_commitish: $sha, name: $name, body: "Automated Windows release from the prod branch.", draft: false, prerelease: false}')"
|
--rawfile body release-notes.md \
|
||||||
|
'{
|
||||||
|
tag_name: $tag,
|
||||||
|
target_commitish: $sha,
|
||||||
|
name: $name,
|
||||||
|
body: $body,
|
||||||
|
draft: false,
|
||||||
|
prerelease: false
|
||||||
|
}')"
|
||||||
|
|
||||||
release="$(curl --fail-with-body --silent --show-error \
|
release="$(curl --fail-with-body --silent --show-error \
|
||||||
-X POST \
|
-X POST \
|
||||||
@@ -85,10 +402,21 @@ jobs:
|
|||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
--data "$payload" \
|
--data "$payload" \
|
||||||
"${api}/releases")"
|
"${api}/releases")"
|
||||||
|
|
||||||
release_id="$(printf '%s' "$release" | jq -er '.id')"
|
release_id="$(printf '%s' "$release" | jq -er '.id')"
|
||||||
|
|
||||||
curl --fail-with-body --silent --show-error \
|
for asset in release-assets/*; do
|
||||||
-X POST \
|
if [ ! -f "$asset" ]; then
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
continue
|
||||||
-F "attachment=@dist/Gitree-windows-x64.exe" \
|
fi
|
||||||
"${api}/releases/${release_id}/assets?name=Gitree-windows-x64.exe"
|
|
||||||
|
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
|
||||||
|
|||||||
12
.gitmodules
vendored
12
.gitmodules
vendored
@@ -1,15 +1,15 @@
|
|||||||
[submodule "vendor/libgit2"]
|
[submodule "vendor/libgit2"]
|
||||||
path = vendor/libgit2
|
path = vendor/libgit2
|
||||||
url = https://github.com/libgit2/libgit2.git
|
url = https://dock-it.dev/Idea-Studios/libgit2
|
||||||
[submodule "vendor/imgui"]
|
[submodule "vendor/imgui"]
|
||||||
path = vendor/imgui
|
path = vendor/imgui
|
||||||
url = https://github.com/ocornut/imgui.git
|
url = https://dock-it.dev/Idea-Studios/imgui
|
||||||
[submodule "vendor/glfw"]
|
[submodule "vendor/glfw"]
|
||||||
path = vendor/glfw
|
path = vendor/glfw
|
||||||
url = https://github.com/glfw/glfw.git
|
url = https://dock-it.dev/Idea-Studios/glfw
|
||||||
[submodule "vendor/iZo"]
|
[submodule "vendor/iZo"]
|
||||||
path = vendor/iZo
|
path = vendor/iZo
|
||||||
url = https://dock-it.dev/Idea-Studios/iZo
|
url = https://dock-it.dev/Idea-Studios/iZo
|
||||||
[submodule "vendor/iKv"]
|
[submodule "vendor/iKvxx"]
|
||||||
path = vendor/iKv
|
path = vendor/iKvxx
|
||||||
url = https://dock-it.dev/Idea-Studios/iKv
|
url = https://dock-it.dev/Idea-Studios/iKvxx
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE)
|
|||||||
set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||||
set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||||
set(GLFW_INSTALL OFF CACHE BOOL "" FORCE)
|
set(GLFW_INSTALL OFF CACHE BOOL "" FORCE)
|
||||||
|
if(UNIX AND NOT APPLE)
|
||||||
|
set(GLFW_BUILD_X11 ON CACHE BOOL "" FORCE)
|
||||||
|
set(GLFW_BUILD_WAYLAND OFF CACHE BOOL "" FORCE)
|
||||||
|
endif()
|
||||||
add_subdirectory(vendor/glfw EXCLUDE_FROM_ALL)
|
add_subdirectory(vendor/glfw EXCLUDE_FROM_ALL)
|
||||||
|
|
||||||
# Dear ImGui does not ship a CMake target, so keep its static-library recipe here.
|
# Dear ImGui does not ship a CMake target, so keep its static-library recipe here.
|
||||||
@@ -46,11 +50,9 @@ add_subdirectory(vendor/libgit2 EXCLUDE_FROM_ALL)
|
|||||||
set(IZO_BUILD_EXAMPLE OFF CACHE BOOL "" FORCE)
|
set(IZO_BUILD_EXAMPLE OFF CACHE BOOL "" FORCE)
|
||||||
add_subdirectory(vendor/iZo EXCLUDE_FROM_ALL)
|
add_subdirectory(vendor/iZo EXCLUDE_FROM_ALL)
|
||||||
|
|
||||||
# Persistent application settings and session data.
|
# Persistent application settings and session data (C++17 bindings for iKv).
|
||||||
set(IKV_BUILD_DEMOS OFF CACHE BOOL "" FORCE)
|
set(IKVXX_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||||
set(IKV_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
add_subdirectory(vendor/iKvxx EXCLUDE_FROM_ALL)
|
||||||
set(IKV_INSTALL OFF CACHE BOOL "" FORCE)
|
|
||||||
add_subdirectory(vendor/iKv EXCLUDE_FROM_ALL)
|
|
||||||
|
|
||||||
find_package(OpenGL REQUIRED)
|
find_package(OpenGL REQUIRED)
|
||||||
|
|
||||||
@@ -72,22 +74,25 @@ add_executable(gitree WIN32
|
|||||||
src/managers/avatar_cache.h
|
src/managers/avatar_cache.h
|
||||||
src/managers/application_manager.cpp
|
src/managers/application_manager.cpp
|
||||||
src/managers/application_manager.h
|
src/managers/application_manager.h
|
||||||
|
src/managers/application_icon_cache.cpp
|
||||||
|
src/managers/application_icon_cache.h
|
||||||
src/models/repository.h
|
src/models/repository.h
|
||||||
)
|
)
|
||||||
target_include_directories(gitree PRIVATE src vendor/libgit2/include vendor/icons)
|
target_include_directories(gitree PRIVATE src vendor/libgit2/include vendor/icons)
|
||||||
target_link_libraries(gitree PRIVATE imgui libgit2package iZo::izo ikv::ikv OpenGL::GL)
|
target_link_libraries(gitree PRIVATE imgui libgit2package iZo::izo ikvxx::ikvxx OpenGL::GL)
|
||||||
target_compile_definitions(gitree PRIVATE
|
target_compile_definitions(gitree PRIVATE
|
||||||
GITREE_VERSION="${PROJECT_VERSION}"
|
GITREE_VERSION="${PROJECT_VERSION}"
|
||||||
GITREE_ASSET_DIR="${CMAKE_CURRENT_SOURCE_DIR}/vendor/fonts"
|
GITREE_ASSET_DIR="${CMAKE_CURRENT_SOURCE_DIR}/vendor/fonts"
|
||||||
|
GITREE_IMAGE_ASSET_DIR="${CMAKE_CURRENT_SOURCE_DIR}/assets"
|
||||||
$<$<PLATFORM_ID:Windows>:NOMINMAX;WIN32_LEAN_AND_MEAN>
|
$<$<PLATFORM_ID:Windows>:NOMINMAX;WIN32_LEAN_AND_MEAN>
|
||||||
)
|
)
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
target_link_libraries(gitree PRIVATE dwmapi urlmon windowscodecs bcrypt)
|
target_link_libraries(gitree PRIVATE dwmapi urlmon windowscodecs bcrypt crypt32)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
target_compile_options(gitree PRIVATE /W4 /permissive-)
|
target_compile_options(gitree PRIVATE /W4 /permissive- /MP)
|
||||||
else()
|
else()
|
||||||
target_compile_options(gitree PRIVATE -Wall -Wextra -Wpedantic)
|
target_compile_options(gitree PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -1,8 +1,8 @@
|
|||||||
# Gitree
|

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

|
||||||
|
|||||||
6
run.bat
6
run.bat
@@ -14,8 +14,10 @@ if %errorlevel%==0 (
|
|||||||
set "GENERATOR="
|
set "GENERATOR="
|
||||||
)
|
)
|
||||||
|
|
||||||
cmake -S . -B build %GENERATOR% -DCMAKE_BUILD_TYPE=Release || exit /b 1
|
if not exist build\CMakeCache.txt (
|
||||||
cmake --build build --config Release --parallel || exit /b 1
|
cmake -S . -B build %GENERATOR% -DCMAKE_BUILD_TYPE=Release || exit /b 1
|
||||||
|
)
|
||||||
|
cmake --build build --config Release --parallel %NUMBER_OF_PROCESSORS% || exit /b 1
|
||||||
|
|
||||||
if exist build\bin\gitree.exe (
|
if exist build\bin\gitree.exe (
|
||||||
start "" build\bin\gitree.exe %*
|
start "" build\bin\gitree.exe %*
|
||||||
|
|||||||
97
src/managers/application_icon_cache.cpp
Normal file
97
src/managers/application_icon_cache.cpp
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
#include "application_icon_cache.h"
|
||||||
|
|
||||||
|
#include <GLFW/glfw3.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#include <shellapi.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
ApplicationIconCache::~ApplicationIconCache()
|
||||||
|
{
|
||||||
|
shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int ApplicationIconCache::textureFor(const std::filesystem::path &executable)
|
||||||
|
{
|
||||||
|
if (executable.empty())
|
||||||
|
return 0;
|
||||||
|
const std::string key = executable.string();
|
||||||
|
const auto found = textures_.find(key);
|
||||||
|
if (found != textures_.end())
|
||||||
|
return found->second;
|
||||||
|
const unsigned int texture = loadTexture(executable);
|
||||||
|
textures_.emplace(key, texture);
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int ApplicationIconCache::loadTexture(const std::filesystem::path &executable) const
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
HICON large_icon = nullptr;
|
||||||
|
HICON small_icon = nullptr;
|
||||||
|
if (ExtractIconExW(executable.wstring().c_str(), 0, &large_icon, &small_icon, 1) == 0)
|
||||||
|
return 0;
|
||||||
|
HICON icon = large_icon ? large_icon : small_icon;
|
||||||
|
if (!icon)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
constexpr int size = 32;
|
||||||
|
BITMAPINFO bitmap_info{};
|
||||||
|
bitmap_info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||||
|
bitmap_info.bmiHeader.biWidth = size;
|
||||||
|
bitmap_info.bmiHeader.biHeight = -size;
|
||||||
|
bitmap_info.bmiHeader.biPlanes = 1;
|
||||||
|
bitmap_info.bmiHeader.biBitCount = 32;
|
||||||
|
bitmap_info.bmiHeader.biCompression = BI_RGB;
|
||||||
|
|
||||||
|
void *pixels = nullptr;
|
||||||
|
HDC device = CreateCompatibleDC(nullptr);
|
||||||
|
HBITMAP bitmap = CreateDIBSection(device, &bitmap_info, DIB_RGB_COLORS, &pixels, nullptr, 0);
|
||||||
|
unsigned int texture = 0;
|
||||||
|
if (bitmap && pixels)
|
||||||
|
{
|
||||||
|
const HGDIOBJ previous = SelectObject(device, bitmap);
|
||||||
|
std::fill_n(static_cast<unsigned char *>(pixels), size * size * 4, 0);
|
||||||
|
if (DrawIconEx(device, 0, 0, icon, size, size, 0, nullptr, DI_NORMAL))
|
||||||
|
{
|
||||||
|
auto *rgba = static_cast<unsigned char *>(pixels);
|
||||||
|
for (int pixel = 0; pixel < size * size; ++pixel)
|
||||||
|
std::swap(rgba[pixel * 4], rgba[pixel * 4 + 2]);
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size, size, 0,
|
||||||
|
GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
}
|
||||||
|
SelectObject(device, previous);
|
||||||
|
}
|
||||||
|
if (bitmap)
|
||||||
|
DeleteObject(bitmap);
|
||||||
|
if (device)
|
||||||
|
DeleteDC(device);
|
||||||
|
if (large_icon)
|
||||||
|
DestroyIcon(large_icon);
|
||||||
|
if (small_icon)
|
||||||
|
DestroyIcon(small_icon);
|
||||||
|
return texture;
|
||||||
|
#else
|
||||||
|
(void)executable;
|
||||||
|
return 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApplicationIconCache::shutdown()
|
||||||
|
{
|
||||||
|
for (auto &[path, texture] : textures_)
|
||||||
|
{
|
||||||
|
(void)path;
|
||||||
|
if (texture)
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
}
|
||||||
|
textures_.clear();
|
||||||
|
}
|
||||||
22
src/managers/application_icon_cache.h
Normal file
22
src/managers/application_icon_cache.h
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class ApplicationIconCache
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
~ApplicationIconCache();
|
||||||
|
|
||||||
|
ApplicationIconCache(const ApplicationIconCache &) = delete;
|
||||||
|
ApplicationIconCache &operator=(const ApplicationIconCache &) = delete;
|
||||||
|
ApplicationIconCache() = default;
|
||||||
|
|
||||||
|
unsigned int textureFor(const std::filesystem::path &executable);
|
||||||
|
void shutdown();
|
||||||
|
|
||||||
|
private:
|
||||||
|
unsigned int loadTexture(const std::filesystem::path &executable) const;
|
||||||
|
std::map<std::string, unsigned int> textures_;
|
||||||
|
};
|
||||||
@@ -1,122 +1,552 @@
|
|||||||
#include "managers/application_manager.h"
|
#include "managers/application_manager.h"
|
||||||
|
|
||||||
|
#include <izo/Environment.hpp>
|
||||||
|
#include <izo/Paths.hpp>
|
||||||
#include <izo/Process.hpp>
|
#include <izo/Process.hpp>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdlib>
|
|
||||||
|
|
||||||
namespace {
|
namespace
|
||||||
std::filesystem::path environmentPath(const char* name) {
|
{
|
||||||
const char* value = std::getenv(name);
|
std::filesystem::path environmentPath(const char *name)
|
||||||
return value && *value ? std::filesystem::path(value) : std::filesystem::path{};
|
{
|
||||||
}
|
const auto value = izo::GetEnvVar(name);
|
||||||
|
return value && !value->empty() ? std::filesystem::path(*value) : std::filesystem::path{};
|
||||||
std::filesystem::path firstExisting(std::initializer_list<std::filesystem::path> candidates) {
|
}
|
||||||
std::error_code error;
|
|
||||||
for (const auto& candidate : candidates)
|
std::filesystem::path under(const std::filesystem::path &root, const char *relative)
|
||||||
if (!candidate.empty() && std::filesystem::is_regular_file(candidate, error)) return candidate;
|
{
|
||||||
return {};
|
return root.empty() ? std::filesystem::path{} : root / relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::filesystem::path findExecutable(const std::filesystem::path& root, const char* filename) {
|
std::filesystem::path firstExisting(std::initializer_list<std::filesystem::path> candidates)
|
||||||
std::error_code error;
|
{
|
||||||
if (root.empty() || !std::filesystem::is_directory(root, error)) return {};
|
std::error_code error;
|
||||||
for (std::filesystem::recursive_directory_iterator iterator(
|
|
||||||
root, std::filesystem::directory_options::skip_permission_denied, error), end;
|
for (const auto &candidate : candidates)
|
||||||
iterator != end && !error; iterator.increment(error)) {
|
{
|
||||||
if (iterator->is_regular_file(error) && iterator->path().filename() == filename)
|
if (!candidate.empty() && std::filesystem::is_regular_file(candidate, error))
|
||||||
return iterator->path();
|
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;
|
||||||
|
iterator != end && !error; iterator.increment(error))
|
||||||
|
{
|
||||||
|
if (iterator->is_regular_file(error) && iterator->path().filename() == filename)
|
||||||
|
return iterator->path();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplicationManager::ApplicationManager() {
|
ApplicationManager::ApplicationManager()
|
||||||
const auto local = environmentPath("LOCALAPPDATA");
|
{
|
||||||
|
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 = environmentPath("ProgramFiles");
|
||||||
const auto program_files_x86 = environmentPath("ProgramFiles(x86)");
|
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 windows = environmentPath("WINDIR");
|
||||||
|
const auto chocolatey = environmentPath("ChocolateyInstall");
|
||||||
|
|
||||||
auto add = [this](ExternalApplicationId id, const char* name, std::filesystem::path executable,
|
auto add = [this](ExternalApplicationId id, const char *name, std::filesystem::path executable,
|
||||||
std::vector<std::string> arguments = {}) {
|
std::vector<std::string> arguments = {})
|
||||||
|
{
|
||||||
const bool available = !executable.empty();
|
const bool available = !executable.empty();
|
||||||
targets_.push_back({{id, name, available}, std::move(executable), std::move(arguments)});
|
targets_.push_back({{id, name, available, std::move(executable)}, std::move(arguments)});
|
||||||
applications_.push_back(targets_.back().info);
|
applications_.push_back(targets_.back().info);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// VS Code-style editors
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
add(ExternalApplicationId::visual_studio_code, "VS Code", firstExisting({
|
add(ExternalApplicationId::visual_studio_code, "VS Code", firstExisting({
|
||||||
local / "Programs/Microsoft VS Code/Code.exe",
|
under(local, "Programs/Microsoft VS Code/Code.exe"),
|
||||||
program_files / "Microsoft VS Code/Code.exe",
|
under(program_files, "Microsoft VS Code/Code.exe"),
|
||||||
program_files_x86 / "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, "Visual Studio", firstExisting({
|
|
||||||
program_files / "Microsoft Visual Studio/2022/Community/Common7/IDE/devenv.exe",
|
add(ExternalApplicationId::visual_studio_code_insiders, "VS Code Insiders", firstExisting({
|
||||||
program_files / "Microsoft Visual Studio/2022/Professional/Common7/IDE/devenv.exe",
|
under(local, "Programs/Microsoft VS Code Insiders/Code - Insiders.exe"),
|
||||||
program_files / "Microsoft Visual Studio/2022/Enterprise/Common7/IDE/devenv.exe",
|
under(program_files, "Microsoft VS Code Insiders/Code - Insiders.exe"),
|
||||||
program_files_x86 / "Microsoft Visual Studio/2019/Community/Common7/IDE/devenv.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({
|
add(ExternalApplicationId::antigravity, "Antigravity", firstExisting({
|
||||||
local / "Programs/Antigravity/Antigravity.exe",
|
under(local, "Programs/Antigravity/Antigravity.exe"),
|
||||||
local / "Antigravity/Antigravity.exe",
|
under(local, "Antigravity/Antigravity.exe"),
|
||||||
program_files / "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({
|
std::filesystem::path github_desktop = firstExisting({
|
||||||
local / "GitHubDesktop/GitHubDesktop.exe",
|
under(local, "GitHubDesktop/GitHubDesktop.exe"),
|
||||||
local / "GitHub Desktop/GitHubDesktop.exe",
|
under(local, "GitHub Desktop/GitHubDesktop.exe"),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (github_desktop.empty())
|
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::github_desktop, "GitHub Desktop", std::move(github_desktop));
|
||||||
add(ExternalApplicationId::file_explorer, "File Explorer",
|
|
||||||
firstExisting({windows / "explorer.exe"}));
|
add(ExternalApplicationId::file_explorer, "File Explorer", firstExisting({
|
||||||
add(ExternalApplicationId::terminal, "Terminal",
|
under(windows, "explorer.exe"),
|
||||||
firstExisting({local / "Microsoft/WindowsApps/wt.exe", 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"});
|
|
||||||
add(ExternalApplicationId::android_studio, "Android Studio", firstExisting({
|
|
||||||
program_files / "Android/Android Studio/bin/studio64.exe",
|
|
||||||
local / "Programs/Android Studio/bin/studio64.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({
|
||||||
|
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({
|
||||||
|
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({
|
std::filesystem::path idea = firstExisting({
|
||||||
program_files / "JetBrains/IntelliJ IDEA 2025.1/bin/idea64.exe",
|
under(program_files, "JetBrains/IntelliJ IDEA 2026.1/bin/idea64.exe"),
|
||||||
program_files / "JetBrains/IntelliJ IDEA 2024.3/bin/idea64.exe",
|
under(program_files, "JetBrains/IntelliJ IDEA 2025.3/bin/idea64.exe"),
|
||||||
local / "Programs/IntelliJ IDEA Ultimate/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");
|
|
||||||
|
if (idea.empty())
|
||||||
|
idea = findExecutable(under(local, "JetBrains/Toolbox/apps"), "idea64.exe");
|
||||||
|
|
||||||
add(ExternalApplicationId::intellij_idea, "IntelliJ IDEA", std::move(idea));
|
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"),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
ExternalApplicationId ApplicationManager::defaultApplication() const {
|
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 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 auto available = std::find_if(targets_.begin(), targets_.end(),
|
||||||
[](const LaunchTarget& target) { return target.info.available; });
|
[](const LaunchTarget &target)
|
||||||
|
{
|
||||||
|
return target.info.available;
|
||||||
|
});
|
||||||
|
|
||||||
return available == targets_.end()
|
return available == targets_.end()
|
||||||
? ExternalApplicationId::file_explorer
|
? ExternalApplicationId::file_explorer
|
||||||
: available->info.id;
|
: available->info.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ApplicationManager::launch(ExternalApplicationId application,
|
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(),
|
const auto found = std::find_if(targets_.begin(), targets_.end(),
|
||||||
[application](const LaunchTarget& target) { return target.info.id == application; });
|
[application](const LaunchTarget &target)
|
||||||
if (found == targets_.end() || !found->info.available) {
|
{
|
||||||
|
return target.info.id == application;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (found == targets_.end() || !found->info.available)
|
||||||
|
{
|
||||||
error = "The selected application is not installed";
|
error = "The selected application is not installed";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> arguments = found->arguments;
|
std::vector<std::string> arguments = found->arguments;
|
||||||
const std::string repository_path = repository.string();
|
const std::string repository_path = repository.string();
|
||||||
|
|
||||||
if (application == ExternalApplicationId::git_bash && !arguments.empty())
|
if (application == ExternalApplicationId::git_bash && !arguments.empty())
|
||||||
|
{
|
||||||
arguments.back() += repository_path;
|
arguments.back() += repository_path;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
arguments.push_back(repository_path);
|
arguments.push_back(repository_path);
|
||||||
|
}
|
||||||
|
|
||||||
const izo::ProcessResult result = izo::LaunchProcess({
|
const izo::ProcessResult result = izo::LaunchProcess({
|
||||||
found->executable, std::move(arguments), repository, true,
|
found->info.executable,
|
||||||
|
std::move(arguments),
|
||||||
|
repository,
|
||||||
|
true,
|
||||||
});
|
});
|
||||||
if (!result) {
|
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
error = result.errorMessage.empty() ? "Unable to launch application" : result.errorMessage;
|
error = result.errorMessage.empty() ? "Unable to launch application" : result.errorMessage;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
error = "Opened repository in " + found->info.name;
|
error = "Opened repository in " + found->info.name;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -4,41 +4,86 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
enum class ExternalApplicationId {
|
enum class ExternalApplicationId
|
||||||
|
{
|
||||||
visual_studio_code,
|
visual_studio_code,
|
||||||
visual_studio,
|
visual_studio_code_insiders,
|
||||||
|
vscodium,
|
||||||
|
cursor,
|
||||||
|
windsurf,
|
||||||
|
trae,
|
||||||
|
zed,
|
||||||
antigravity,
|
antigravity,
|
||||||
|
|
||||||
|
visual_studio,
|
||||||
|
|
||||||
github_desktop,
|
github_desktop,
|
||||||
file_explorer,
|
file_explorer,
|
||||||
terminal,
|
terminal,
|
||||||
git_bash,
|
git_bash,
|
||||||
wsl,
|
wsl,
|
||||||
|
|
||||||
android_studio,
|
android_studio,
|
||||||
|
|
||||||
intellij_idea,
|
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 {
|
struct ExternalApplication
|
||||||
|
{
|
||||||
ExternalApplicationId id;
|
ExternalApplicationId id;
|
||||||
std::string name;
|
std::string name;
|
||||||
bool available = false;
|
bool available = false;
|
||||||
|
std::filesystem::path executable;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ApplicationManager {
|
class ApplicationManager
|
||||||
|
{
|
||||||
public:
|
public:
|
||||||
ApplicationManager();
|
ApplicationManager();
|
||||||
|
|
||||||
const std::vector<ExternalApplication>& applications() const { return applications_; }
|
const std::vector<ExternalApplication> &applications() const { return applications_; }
|
||||||
|
const ExternalApplication *application(ExternalApplicationId id) const;
|
||||||
ExternalApplicationId defaultApplication() const;
|
ExternalApplicationId defaultApplication() const;
|
||||||
bool launch(ExternalApplicationId application, const std::filesystem::path& repository,
|
bool launch(ExternalApplicationId application, const std::filesystem::path &repository,
|
||||||
std::string& error) const;
|
std::string &error) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct LaunchTarget {
|
struct LaunchTarget
|
||||||
|
{
|
||||||
ExternalApplication info;
|
ExternalApplication info;
|
||||||
std::filesystem::path executable;
|
|
||||||
std::vector<std::string> arguments;
|
std::vector<std::string> arguments;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::vector<LaunchTarget> targets_;
|
std::vector<LaunchTarget> targets_;
|
||||||
std::vector<ExternalApplication> applications_;
|
std::vector<ExternalApplication> applications_;
|
||||||
};
|
};
|
||||||
@@ -17,23 +17,31 @@
|
|||||||
#include <wincodec.h>
|
#include <wincodec.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
AvatarCache::AvatarCache(const std::filesystem::path& user_data_directory)
|
AvatarCache::AvatarCache(const std::filesystem::path &user_data_directory)
|
||||||
: directory_(user_data_directory / "avatars") {
|
: directory_(user_data_directory / "avatars")
|
||||||
|
{
|
||||||
std::filesystem::create_directories(directory_);
|
std::filesystem::create_directories(directory_);
|
||||||
}
|
}
|
||||||
|
|
||||||
AvatarCache::~AvatarCache() {
|
AvatarCache::~AvatarCache()
|
||||||
|
{
|
||||||
shutdown();
|
shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string AvatarCache::hashEmail(const std::string& email) const {
|
std::string AvatarCache::hashEmail(const std::string &email) const
|
||||||
|
{
|
||||||
std::string normalized = email;
|
std::string normalized = email;
|
||||||
normalized.erase(normalized.begin(), std::find_if(normalized.begin(), normalized.end(),
|
normalized.erase(normalized.begin(), std::find_if(normalized.begin(), normalized.end(),
|
||||||
[](unsigned char value) { return !std::isspace(value); }));
|
[](unsigned char value)
|
||||||
|
{ return !std::isspace(value); }));
|
||||||
normalized.erase(std::find_if(normalized.rbegin(), normalized.rend(),
|
normalized.erase(std::find_if(normalized.rbegin(), normalized.rend(),
|
||||||
[](unsigned char value) { return !std::isspace(value); }).base(), normalized.end());
|
[](unsigned char value)
|
||||||
|
{ return !std::isspace(value); })
|
||||||
|
.base(),
|
||||||
|
normalized.end());
|
||||||
std::transform(normalized.begin(), normalized.end(), normalized.begin(),
|
std::transform(normalized.begin(), normalized.end(), normalized.begin(),
|
||||||
[](unsigned char value) { return static_cast<char>(std::tolower(value)); });
|
[](unsigned char value)
|
||||||
|
{ return static_cast<char>(std::tolower(value)); });
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
BCRYPT_ALG_HANDLE algorithm = nullptr;
|
BCRYPT_ALG_HANDLE algorithm = nullptr;
|
||||||
@@ -43,99 +51,121 @@ std::string AvatarCache::hashEmail(const std::string& email) const {
|
|||||||
DWORD written = 0;
|
DWORD written = 0;
|
||||||
if (BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_MD5_ALGORITHM, nullptr, 0) < 0 ||
|
if (BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_MD5_ALGORITHM, nullptr, 0) < 0 ||
|
||||||
BCryptGetProperty(algorithm, BCRYPT_OBJECT_LENGTH, reinterpret_cast<PUCHAR>(&object_size),
|
BCryptGetProperty(algorithm, BCRYPT_OBJECT_LENGTH, reinterpret_cast<PUCHAR>(&object_size),
|
||||||
sizeof(object_size), &written, 0) < 0) {
|
sizeof(object_size), &written, 0) < 0)
|
||||||
if (algorithm) BCryptCloseAlgorithmProvider(algorithm, 0);
|
{
|
||||||
|
if (algorithm)
|
||||||
|
BCryptCloseAlgorithmProvider(algorithm, 0);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
std::vector<unsigned char> object(object_size);
|
std::vector<unsigned char> object(object_size);
|
||||||
if (BCryptCreateHash(algorithm, &hash, object.data(), object_size, nullptr, 0, 0) < 0 ||
|
if (BCryptCreateHash(algorithm, &hash, object.data(), object_size, nullptr, 0, 0) < 0 ||
|
||||||
BCryptHashData(hash, reinterpret_cast<PUCHAR>(normalized.data()),
|
BCryptHashData(hash, reinterpret_cast<PUCHAR>(normalized.data()),
|
||||||
static_cast<ULONG>(normalized.size()), 0) < 0 ||
|
static_cast<ULONG>(normalized.size()), 0) < 0 ||
|
||||||
BCryptFinishHash(hash, digest.data(), static_cast<ULONG>(digest.size()), 0) < 0) {
|
BCryptFinishHash(hash, digest.data(), static_cast<ULONG>(digest.size()), 0) < 0)
|
||||||
if (hash) BCryptDestroyHash(hash);
|
{
|
||||||
|
if (hash)
|
||||||
|
BCryptDestroyHash(hash);
|
||||||
BCryptCloseAlgorithmProvider(algorithm, 0);
|
BCryptCloseAlgorithmProvider(algorithm, 0);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
BCryptDestroyHash(hash);
|
BCryptDestroyHash(hash);
|
||||||
BCryptCloseAlgorithmProvider(algorithm, 0);
|
BCryptCloseAlgorithmProvider(algorithm, 0);
|
||||||
std::ostringstream output;
|
std::ostringstream output;
|
||||||
for (unsigned char value : digest) output << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(value);
|
for (unsigned char value : digest)
|
||||||
|
output << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(value);
|
||||||
return output.str();
|
return output.str();
|
||||||
#else
|
#else
|
||||||
return std::to_string(std::hash<std::string>{}(normalized));
|
return std::to_string(std::hash<std::string>{}(normalized));
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
unsigned int AvatarCache::textureFor(const std::string& email) {
|
unsigned int AvatarCache::textureFor(const std::string &email)
|
||||||
if (email.empty()) return 0;
|
{
|
||||||
|
if (email.empty())
|
||||||
|
return 0;
|
||||||
const std::string hash = hashEmail(email);
|
const std::string hash = hashEmail(email);
|
||||||
if (hash.empty()) return 0;
|
if (hash.empty())
|
||||||
Entry& entry = entries_[hash];
|
return 0;
|
||||||
if (entry.texture) return entry.texture;
|
Entry &entry = entries_[hash];
|
||||||
if (entry.requested) return 0;
|
if (entry.texture)
|
||||||
|
return entry.texture;
|
||||||
|
if (entry.requested)
|
||||||
|
return 0;
|
||||||
|
|
||||||
entry.requested = true;
|
entry.requested = true;
|
||||||
entry.file = directory_ / (hash + ".img");
|
entry.file = directory_ / (hash + ".img");
|
||||||
if (std::filesystem::exists(entry.file)) {
|
if (std::filesystem::exists(entry.file))
|
||||||
|
{
|
||||||
entry.texture = loadTexture(entry.file);
|
entry.texture = loadTexture(entry.file);
|
||||||
return entry.texture;
|
return entry.texture;
|
||||||
}
|
}
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
const std::filesystem::path file = entry.file;
|
const std::filesystem::path file = entry.file;
|
||||||
const std::wstring url = L"https://www.gravatar.com/avatar/" +
|
const std::wstring url = L"https://www.gravatar.com/avatar/" +
|
||||||
std::wstring(hash.begin(), hash.end()) + L"?s=64&d=identicon&r=g";
|
std::wstring(hash.begin(), hash.end()) + L"?s=64&d=identicon&r=g";
|
||||||
entry.download = std::async(std::launch::async, [file, url] {
|
entry.download = std::async(std::launch::async, [file, url]
|
||||||
return SUCCEEDED(URLDownloadToFileW(nullptr, url.c_str(), file.wstring().c_str(), 0, nullptr));
|
{ return SUCCEEDED(URLDownloadToFileW(nullptr, url.c_str(), file.wstring().c_str(), 0, nullptr)); });
|
||||||
});
|
|
||||||
#endif
|
#endif
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AvatarCache::update() {
|
void AvatarCache::update()
|
||||||
for (auto& [hash, entry] : entries_) {
|
{
|
||||||
|
for (auto &[hash, entry] : entries_)
|
||||||
|
{
|
||||||
(void)hash;
|
(void)hash;
|
||||||
if (entry.texture || !entry.download.valid()) continue;
|
if (entry.texture || !entry.download.valid())
|
||||||
if (entry.download.wait_for(std::chrono::seconds(0)) != std::future_status::ready) continue;
|
continue;
|
||||||
if (entry.download.get()) entry.texture = loadTexture(entry.file);
|
if (entry.download.wait_for(std::chrono::seconds(0)) != std::future_status::ready)
|
||||||
|
continue;
|
||||||
|
if (entry.download.get())
|
||||||
|
entry.texture = loadTexture(entry.file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unsigned int AvatarCache::loadTexture(const std::filesystem::path& file) const {
|
unsigned int AvatarCache::loadTexture(const std::filesystem::path &file) const
|
||||||
|
{
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||||
IWICImagingFactory* factory = nullptr;
|
IWICImagingFactory *factory = nullptr;
|
||||||
IWICBitmapDecoder* decoder = nullptr;
|
IWICBitmapDecoder *decoder = nullptr;
|
||||||
IWICBitmapFrameDecode* frame = nullptr;
|
IWICBitmapFrameDecode *frame = nullptr;
|
||||||
IWICFormatConverter* converter = nullptr;
|
IWICFormatConverter *converter = nullptr;
|
||||||
UINT width = 0;
|
UINT width = 0;
|
||||||
UINT height = 0;
|
UINT height = 0;
|
||||||
std::vector<unsigned char> pixels;
|
std::vector<unsigned char> pixels;
|
||||||
unsigned int texture = 0;
|
unsigned int texture = 0;
|
||||||
if (SUCCEEDED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER,
|
if (SUCCEEDED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER,
|
||||||
IID_PPV_ARGS(&factory))) &&
|
IID_PPV_ARGS(&factory))) &&
|
||||||
SUCCEEDED(factory->CreateDecoderFromFilename(file.wstring().c_str(), nullptr, GENERIC_READ,
|
SUCCEEDED(factory->CreateDecoderFromFilename(file.wstring().c_str(), nullptr, GENERIC_READ,
|
||||||
WICDecodeMetadataCacheOnLoad, &decoder)) &&
|
WICDecodeMetadataCacheOnLoad, &decoder)) &&
|
||||||
SUCCEEDED(decoder->GetFrame(0, &frame)) &&
|
SUCCEEDED(decoder->GetFrame(0, &frame)) &&
|
||||||
SUCCEEDED(factory->CreateFormatConverter(&converter)) &&
|
SUCCEEDED(factory->CreateFormatConverter(&converter)) &&
|
||||||
SUCCEEDED(converter->Initialize(frame, GUID_WICPixelFormat32bppRGBA,
|
SUCCEEDED(converter->Initialize(frame, GUID_WICPixelFormat32bppRGBA,
|
||||||
WICBitmapDitherTypeNone, nullptr, 0.0, WICBitmapPaletteTypeCustom)) &&
|
WICBitmapDitherTypeNone, nullptr, 0.0, WICBitmapPaletteTypeCustom)) &&
|
||||||
SUCCEEDED(converter->GetSize(&width, &height))) {
|
SUCCEEDED(converter->GetSize(&width, &height)))
|
||||||
|
{
|
||||||
pixels.resize(static_cast<size_t>(width) * height * 4);
|
pixels.resize(static_cast<size_t>(width) * height * 4);
|
||||||
if (SUCCEEDED(converter->CopyPixels(nullptr, width * 4,
|
if (SUCCEEDED(converter->CopyPixels(nullptr, width * 4,
|
||||||
static_cast<UINT>(pixels.size()), pixels.data()))) {
|
static_cast<UINT>(pixels.size()), pixels.data())))
|
||||||
|
{
|
||||||
glGenTextures(1, &texture);
|
glGenTextures(1, &texture);
|
||||||
glBindTexture(GL_TEXTURE_2D, texture);
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, static_cast<int>(width), static_cast<int>(height),
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, static_cast<int>(width), static_cast<int>(height),
|
||||||
0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (converter) converter->Release();
|
if (converter)
|
||||||
if (frame) frame->Release();
|
converter->Release();
|
||||||
if (decoder) decoder->Release();
|
if (frame)
|
||||||
if (factory) factory->Release();
|
frame->Release();
|
||||||
|
if (decoder)
|
||||||
|
decoder->Release();
|
||||||
|
if (factory)
|
||||||
|
factory->Release();
|
||||||
CoUninitialize();
|
CoUninitialize();
|
||||||
return texture;
|
return texture;
|
||||||
#else
|
#else
|
||||||
@@ -144,11 +174,15 @@ unsigned int AvatarCache::loadTexture(const std::filesystem::path& file) const {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void AvatarCache::shutdown() {
|
void AvatarCache::shutdown()
|
||||||
for (auto& [hash, entry] : entries_) {
|
{
|
||||||
|
for (auto &[hash, entry] : entries_)
|
||||||
|
{
|
||||||
(void)hash;
|
(void)hash;
|
||||||
if (entry.download.valid()) entry.download.wait();
|
if (entry.download.valid())
|
||||||
if (entry.texture) glDeleteTextures(1, &entry.texture);
|
entry.download.wait();
|
||||||
|
if (entry.texture)
|
||||||
|
glDeleteTextures(1, &entry.texture);
|
||||||
entry.texture = 0;
|
entry.texture = 0;
|
||||||
}
|
}
|
||||||
entries_.clear();
|
entries_.clear();
|
||||||
|
|||||||
@@ -5,28 +5,30 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
class AvatarCache {
|
class AvatarCache
|
||||||
|
{
|
||||||
public:
|
public:
|
||||||
explicit AvatarCache(const std::filesystem::path& user_data_directory);
|
explicit AvatarCache(const std::filesystem::path &user_data_directory);
|
||||||
~AvatarCache();
|
~AvatarCache();
|
||||||
|
|
||||||
AvatarCache(const AvatarCache&) = delete;
|
AvatarCache(const AvatarCache &) = delete;
|
||||||
AvatarCache& operator=(const AvatarCache&) = delete;
|
AvatarCache &operator=(const AvatarCache &) = delete;
|
||||||
|
|
||||||
unsigned int textureFor(const std::string& email);
|
unsigned int textureFor(const std::string &email);
|
||||||
void update();
|
void update();
|
||||||
void shutdown();
|
void shutdown();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Entry {
|
struct Entry
|
||||||
|
{
|
||||||
unsigned int texture = 0;
|
unsigned int texture = 0;
|
||||||
bool requested = false;
|
bool requested = false;
|
||||||
std::filesystem::path file;
|
std::filesystem::path file;
|
||||||
std::future<bool> download;
|
std::future<bool> download;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string hashEmail(const std::string& email) const;
|
std::string hashEmail(const std::string &email) const;
|
||||||
unsigned int loadTexture(const std::filesystem::path& file) const;
|
unsigned int loadTexture(const std::filesystem::path &file) const;
|
||||||
std::filesystem::path directory_;
|
std::filesystem::path directory_;
|
||||||
std::map<std::string, Entry> entries_;
|
std::map<std::string, Entry> entries_;
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,60 +1,91 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "models/repository.h"
|
#include "models/repository.h"
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
class GitManager {
|
struct GitAuthOverride
|
||||||
|
{
|
||||||
|
std::string remote_url;
|
||||||
|
std::string username;
|
||||||
|
std::string password;
|
||||||
|
};
|
||||||
|
|
||||||
|
class GitManager
|
||||||
|
{
|
||||||
public:
|
public:
|
||||||
GitManager();
|
GitManager();
|
||||||
~GitManager();
|
~GitManager();
|
||||||
|
|
||||||
bool openRepository(RepositoryView& repository, const std::string& path, std::string& error);
|
bool openRepositoryHandle(RepositoryView &repository, const std::string &path, std::string &error);
|
||||||
bool initializeRepository(RepositoryView& repository, const std::string& path, std::string& error);
|
bool openRepository(RepositoryView &repository, const std::string &path, std::string &error);
|
||||||
bool reload(RepositoryView& repository, std::string& error);
|
bool initializeRepository(RepositoryView &repository, const std::string &path,
|
||||||
bool loadMoreCommits(RepositoryView& repository, size_t page_size, std::string& error);
|
const std::string &initial_branch, std::string &error);
|
||||||
bool checkoutBranch(RepositoryView& repository, const std::string& branch, std::string& error);
|
bool cloneRepository(RepositoryView &repository, const std::string &url,
|
||||||
bool fetch(RepositoryView& repository, const std::string& remote, std::string& error);
|
const std::string &path, bool shallow, std::string &error);
|
||||||
bool pull(RepositoryView& repository, int mode, std::string& error);
|
bool reload(RepositoryView &repository, std::string &error);
|
||||||
bool push(RepositoryView& repository, std::string& error);
|
bool loadMoreCommits(RepositoryView &repository, size_t page_size, std::string &error);
|
||||||
bool stash(RepositoryView& repository, std::string& error);
|
bool checkoutBranch(RepositoryView &repository, const std::string &branch, std::string &error);
|
||||||
bool popStash(RepositoryView& repository, std::string& error);
|
bool mergeBranch(RepositoryView &repository, const std::string &source_branch,
|
||||||
bool undoCommit(RepositoryView& repository, std::string& error);
|
const std::string &target_branch, std::string &error);
|
||||||
bool redoCommit(RepositoryView& repository, std::string& error);
|
bool fetch(RepositoryView &repository, const std::string &remote, std::string &error,
|
||||||
bool stageAll(RepositoryView& repository, std::string& error);
|
const std::optional<GitAuthOverride> &auth = std::nullopt);
|
||||||
bool stageFile(RepositoryView& repository, const std::string& path, std::string& error);
|
bool pull(RepositoryView &repository, int mode, std::string &error,
|
||||||
bool unstageFile(RepositoryView& repository, const std::string& path, std::string& error);
|
const std::optional<GitAuthOverride> &auth = std::nullopt);
|
||||||
bool discardFile(RepositoryView& repository, const std::string& path, std::string& error);
|
bool push(RepositoryView &repository, std::string &error,
|
||||||
bool discardAll(RepositoryView& repository, std::string& error);
|
const std::optional<GitAuthOverride> &auth = std::nullopt);
|
||||||
bool commit(RepositoryView& repository, const std::string& summary,
|
bool pushBranch(RepositoryView &repository, const std::string &branch, std::string &error,
|
||||||
const std::string& description, bool amend, std::string& error);
|
const std::optional<GitAuthOverride> &auth = std::nullopt);
|
||||||
bool createBranch(RepositoryView& repository, const std::string& name,
|
bool stash(RepositoryView &repository, std::string &error);
|
||||||
const std::string& start_point, bool checkout, std::string& error);
|
bool popStash(RepositoryView &repository, std::string &error);
|
||||||
bool createTag(RepositoryView& repository, const std::string& name,
|
bool undoCommit(RepositoryView &repository, std::string &error);
|
||||||
const std::string& target, std::string& error);
|
bool redoCommit(RepositoryView &repository, std::string &error);
|
||||||
bool addRemote(RepositoryView& repository, const std::string& name,
|
bool stageAll(RepositoryView &repository, std::string &error);
|
||||||
const std::string& url, std::string& error);
|
bool unstageAll(RepositoryView &repository, std::string &error);
|
||||||
bool addWorktree(RepositoryView& repository, const std::string& path,
|
bool stageFile(RepositoryView &repository, const std::string &path, std::string &error);
|
||||||
const std::string& branch, std::string& error);
|
bool unstageFile(RepositoryView &repository, const std::string &path, std::string &error);
|
||||||
bool addSubmodule(RepositoryView& repository, const std::string& url,
|
bool discardFile(RepositoryView &repository, const std::string &path, std::string &error);
|
||||||
const std::string& path, std::string& error);
|
bool discardAll(RepositoryView &repository, std::string &error);
|
||||||
bool updateSubmodule(RepositoryView& repository, const std::string& name, std::string& error);
|
bool commit(RepositoryView &repository, const std::string &summary,
|
||||||
std::string worktreePath(RepositoryView& repository, const std::string& name, std::string& error);
|
const std::string &description, bool amend, std::string &error);
|
||||||
bool loadCommitChanges(RepositoryView& repository, int commit_index, std::string& error);
|
bool createBranch(RepositoryView &repository, const std::string &name,
|
||||||
bool captureGit(RepositoryView& repository, const std::vector<std::string>& arguments,
|
const std::string &start_point, bool checkout, std::string &error);
|
||||||
std::string& output, std::string& error);
|
bool createTag(RepositoryView &repository, const std::string &name,
|
||||||
bool applyPatch(RepositoryView& repository, const std::string& patch, bool cached,
|
const std::string &target, std::string &error);
|
||||||
bool reverse, 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,
|
||||||
|
const std::string *stdin_data = nullptr);
|
||||||
|
bool applyPatch(RepositoryView &repository, const std::string &patch, bool cached,
|
||||||
|
bool reverse, std::string &error);
|
||||||
|
std::string remoteUrl(RepositoryView &repository, const std::string &remote_name, std::string &error);
|
||||||
|
bool storeCredential(RepositoryView &repository, const std::string &remote_url,
|
||||||
|
const std::string &username, const std::string &password,
|
||||||
|
std::string &error);
|
||||||
|
static bool isAuthenticationFailure(std::string_view message);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static std::string lastError(const char* fallback);
|
static std::string lastError(const char *fallback);
|
||||||
static std::string formatTime(git_time_t value, int offset_minutes);
|
static std::string formatTime(git_time_t value, int offset_minutes);
|
||||||
void readBranches(RepositoryView& repository, git_branch_t type, std::vector<std::string>& output);
|
void loadToolbarHistoryActions(RepositoryView &repository);
|
||||||
void loadRefBadges(RepositoryView& repository);
|
void readBranches(RepositoryView &repository, git_branch_t type, std::vector<std::string> &output);
|
||||||
void computeGraphLanes(RepositoryView& repository);
|
void loadBranchDivergence(RepositoryView &repository);
|
||||||
bool loadRepositoryData(RepositoryView& repository, std::string& error);
|
void loadRefBadges(RepositoryView &repository);
|
||||||
void loadWorkingTree(RepositoryView& repository);
|
void computeGraphLanes(RepositoryView &repository);
|
||||||
bool prepareCredentials(RepositoryView& repository, std::string& error);
|
bool loadRepositoryData(RepositoryView &repository, std::string &error);
|
||||||
bool runGit(RepositoryView& repository, const std::vector<std::string>& arguments,
|
void loadWorkingTree(RepositoryView &repository);
|
||||||
const char* success_message, std::string& message, bool reload = true);
|
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,
|
||||||
|
const std::optional<GitAuthOverride> &auth = std::nullopt);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,155 +1,645 @@
|
|||||||
#include "user_data.h"
|
#include "user_data.h"
|
||||||
|
|
||||||
extern "C" {
|
#include <izo/Paths.hpp>
|
||||||
#include <ikv.h>
|
#include <ikvxx/ikvxx.hpp>
|
||||||
}
|
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdlib>
|
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
|
#include <optional>
|
||||||
|
#include <sstream>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
namespace {
|
|
||||||
std::filesystem::path roaming_directory() {
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
if (const char* appdata = std::getenv("APPDATA")) return appdata;
|
#include <windows.h>
|
||||||
|
#include <wincrypt.h>
|
||||||
#endif
|
#endif
|
||||||
if (const char* home = std::getenv("HOME")) return std::filesystem::path(home) / ".config";
|
|
||||||
return std::filesystem::temp_directory_path();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
using ArrayIndex = ikv::Value::ArrayIndex;
|
||||||
|
|
||||||
const ikv_node_t* object_value(const ikv_node_t* object, const char* key, ikv_type_t type) {
|
std::filesystem::path roaming_directory()
|
||||||
const ikv_node_t* value = object ? ikv_object_get(object, key) : nullptr;
|
{
|
||||||
return value && ikv_node_type(value) == type ? value : nullptr;
|
if (const auto config = izo::GetKnownPath(izo::KnownPath::Config); !config.empty())
|
||||||
}
|
return config;
|
||||||
}
|
if (const auto temporary = izo::GetKnownPath(izo::KnownPath::Temporary); !temporary.empty())
|
||||||
|
return temporary;
|
||||||
|
return std::filesystem::temp_directory_path();
|
||||||
|
}
|
||||||
|
|
||||||
UserData::UserData() {
|
std::optional<std::pair<std::string, std::string>> splitCredentialPayload(const std::string &payload)
|
||||||
|
{
|
||||||
|
const size_t separator = payload.find('\n');
|
||||||
|
if (separator == std::string::npos)
|
||||||
|
return std::nullopt;
|
||||||
|
return std::pair<std::string, std::string>{
|
||||||
|
payload.substr(0, separator),
|
||||||
|
payload.substr(separator + 1)};
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
std::string hexEncode(const std::string &value)
|
||||||
|
{
|
||||||
|
static constexpr char digits[] = "0123456789abcdef";
|
||||||
|
std::string encoded;
|
||||||
|
encoded.reserve(value.size() * 2);
|
||||||
|
for (const unsigned char byte : value)
|
||||||
|
{
|
||||||
|
encoded.push_back(digits[(byte >> 4) & 0x0F]);
|
||||||
|
encoded.push_back(digits[byte & 0x0F]);
|
||||||
|
}
|
||||||
|
return encoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> hexDecode(const std::string &value)
|
||||||
|
{
|
||||||
|
if (value.size() % 2 != 0)
|
||||||
|
return std::nullopt;
|
||||||
|
auto decode_nibble = [](char character) -> int
|
||||||
|
{
|
||||||
|
if (character >= '0' && character <= '9')
|
||||||
|
return character - '0';
|
||||||
|
if (character >= 'a' && character <= 'f')
|
||||||
|
return 10 + (character - 'a');
|
||||||
|
if (character >= 'A' && character <= 'F')
|
||||||
|
return 10 + (character - 'A');
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
std::string decoded;
|
||||||
|
decoded.reserve(value.size() / 2);
|
||||||
|
for (size_t index = 0; index < value.size(); index += 2)
|
||||||
|
{
|
||||||
|
const int high = decode_nibble(value[index]);
|
||||||
|
const int low = decode_nibble(value[index + 1]);
|
||||||
|
if (high < 0 || low < 0)
|
||||||
|
return std::nullopt;
|
||||||
|
decoded.push_back(static_cast<char>((high << 4) | low));
|
||||||
|
}
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> protectCredentialString(const std::string &value)
|
||||||
|
{
|
||||||
|
DATA_BLOB input{};
|
||||||
|
input.pbData = reinterpret_cast<BYTE *>(const_cast<char *>(value.data()));
|
||||||
|
input.cbData = static_cast<DWORD>(value.size());
|
||||||
|
DATA_BLOB output{};
|
||||||
|
if (!CryptProtectData(&input, L"Gitree Git Credential", nullptr, nullptr, nullptr,
|
||||||
|
CRYPTPROTECT_UI_FORBIDDEN, &output))
|
||||||
|
return std::nullopt;
|
||||||
|
std::string protected_value(reinterpret_cast<const char *>(output.pbData), output.cbData);
|
||||||
|
LocalFree(output.pbData);
|
||||||
|
return hexEncode(protected_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> unprotectCredentialString(const std::string &value)
|
||||||
|
{
|
||||||
|
const auto binary = hexDecode(value);
|
||||||
|
if (!binary)
|
||||||
|
return std::nullopt;
|
||||||
|
DATA_BLOB input{};
|
||||||
|
input.pbData = reinterpret_cast<BYTE *>(const_cast<char *>(binary->data()));
|
||||||
|
input.cbData = static_cast<DWORD>(binary->size());
|
||||||
|
DATA_BLOB output{};
|
||||||
|
if (!CryptUnprotectData(&input, nullptr, nullptr, nullptr, nullptr,
|
||||||
|
CRYPTPROTECT_UI_FORBIDDEN, &output))
|
||||||
|
return std::nullopt;
|
||||||
|
std::string plain(reinterpret_cast<const char *>(output.pbData), output.cbData);
|
||||||
|
LocalFree(output.pbData);
|
||||||
|
return plain;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
std::string normalizePath(const std::string &path)
|
||||||
|
{
|
||||||
|
if (path.empty())
|
||||||
|
return {};
|
||||||
|
try
|
||||||
|
{
|
||||||
|
std::filesystem::path p(path);
|
||||||
|
std::string result = p.lexically_normal().generic_string();
|
||||||
|
if (result.size() > 1 && result.back() == '/')
|
||||||
|
result.pop_back();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<float> readFloat(const ikv::Value &value)
|
||||||
|
{
|
||||||
|
if (value.isDouble())
|
||||||
|
return static_cast<float>(value.asDouble());
|
||||||
|
if (value.isInt())
|
||||||
|
return static_cast<float>(value.asInt64());
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load settings from an ikv::Value "settings" object into UserData fields.
|
||||||
|
// Returns true if anything useful was read.
|
||||||
|
void loadSettings(const ikv::Value &settings, float &sidebar_width, float &details_width,
|
||||||
|
bool &sidebar_collapsed, float &commit_message_height,
|
||||||
|
float &working_composer_height, int &pull_mode, int &zoom_percent,
|
||||||
|
std::array<float, 4> &sidebar_section_heights,
|
||||||
|
std::array<bool, 4> &sidebar_section_open,
|
||||||
|
std::array<float, 4> &commit_table_column_widths)
|
||||||
|
{
|
||||||
|
if (settings.isMember("sidebar_width"))
|
||||||
|
if (const auto value = readFloat(settings["sidebar_width"]); value)
|
||||||
|
sidebar_width = *value;
|
||||||
|
if (settings.isMember("details_width"))
|
||||||
|
if (const auto value = readFloat(settings["details_width"]); value)
|
||||||
|
details_width = *value;
|
||||||
|
if (settings.isMember("sidebar_collapsed") && settings["sidebar_collapsed"].isBool())
|
||||||
|
sidebar_collapsed = settings["sidebar_collapsed"].asBool();
|
||||||
|
if (settings.isMember("commit_message_height"))
|
||||||
|
if (const auto value = readFloat(settings["commit_message_height"]); value)
|
||||||
|
commit_message_height = *value;
|
||||||
|
if (settings.isMember("working_composer_height"))
|
||||||
|
if (const auto value = readFloat(settings["working_composer_height"]); value)
|
||||||
|
working_composer_height = *value;
|
||||||
|
if (settings.isMember("pull_mode") && settings["pull_mode"].isInt())
|
||||||
|
pull_mode = static_cast<int>(settings["pull_mode"].asInt64());
|
||||||
|
if (settings.isMember("zoom_percent") && settings["zoom_percent"].isInt())
|
||||||
|
zoom_percent = static_cast<int>(settings["zoom_percent"].asInt64());
|
||||||
|
|
||||||
|
if (settings.isMember("sidebar_sections") && settings["sidebar_sections"].isArray())
|
||||||
|
{
|
||||||
|
const ikv::Value &arr = settings["sidebar_sections"];
|
||||||
|
const auto count = std::min(arr.size(), sidebar_section_heights.size());
|
||||||
|
for (std::size_t i = 0; i < count; ++i)
|
||||||
|
if (const auto value = readFloat(arr[static_cast<ArrayIndex>(i)]); value)
|
||||||
|
sidebar_section_heights[i] = *value;
|
||||||
|
}
|
||||||
|
if (settings.isMember("sidebar_section_open") && settings["sidebar_section_open"].isArray())
|
||||||
|
{
|
||||||
|
const ikv::Value &arr = settings["sidebar_section_open"];
|
||||||
|
const auto count = std::min(arr.size(), sidebar_section_open.size());
|
||||||
|
for (std::size_t i = 0; i < count; ++i)
|
||||||
|
sidebar_section_open[i] = arr[static_cast<ArrayIndex>(i)].asBool();
|
||||||
|
}
|
||||||
|
if (settings.isMember("commit_table_columns") && settings["commit_table_columns"].isArray())
|
||||||
|
{
|
||||||
|
const ikv::Value &arr = settings["commit_table_columns"];
|
||||||
|
const auto count = std::min(arr.size(), commit_table_column_widths.size());
|
||||||
|
for (std::size_t i = 0; i < count; ++i)
|
||||||
|
if (const auto value = readFloat(arr[static_cast<ArrayIndex>(i)]); value)
|
||||||
|
commit_table_column_widths[i] = *value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
UserData::UserData()
|
||||||
|
{
|
||||||
directory_ = roaming_directory() / "Identity" / "Gitree";
|
directory_ = roaming_directory() / "Identity" / "Gitree";
|
||||||
std::filesystem::create_directories(directory_);
|
std::filesystem::create_directories(directory_);
|
||||||
imgui_ini_path_ = (directory_ / "imgui.ini").string();
|
|
||||||
load();
|
load();
|
||||||
}
|
}
|
||||||
|
|
||||||
UserData::~UserData() {
|
UserData::~UserData()
|
||||||
|
{
|
||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
void UserData::load() {
|
std::string UserData::credentialScope(const std::string &remote_url)
|
||||||
const std::filesystem::path data_path = directory_ / "user_data.ikv";
|
{
|
||||||
if (ikv_node_t* root = ikv_parse_file(data_path.string().c_str())) {
|
const size_t scheme = remote_url.find("://");
|
||||||
if (const ikv_node_t* settings = object_value(root, "settings", IKV_OBJECT)) {
|
if (scheme == std::string::npos)
|
||||||
if (const ikv_node_t* value = object_value(settings, "sidebar_width", IKV_FLOAT))
|
return remote_url;
|
||||||
sidebar_width_ = static_cast<float>(ikv_as_float(value));
|
const size_t authority_begin = scheme + 3;
|
||||||
if (const ikv_node_t* value = object_value(settings, "details_width", IKV_FLOAT))
|
const size_t authority_end = remote_url.find_first_of("/?#", authority_begin);
|
||||||
details_width_ = static_cast<float>(ikv_as_float(value));
|
return remote_url.substr(0, authority_end == std::string::npos ? remote_url.size() : authority_end);
|
||||||
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* heights = object_value(settings, "sidebar_sections", IKV_ARRAY)) {
|
std::filesystem::path UserData::dataPath() const
|
||||||
const uint32_t count = std::min<uint32_t>(
|
{
|
||||||
ikv_array_size(heights), static_cast<uint32_t>(sidebar_section_heights_.size()));
|
return directory_ / "user_data.ikv";
|
||||||
for (uint32_t index = 0; index < count; ++index)
|
}
|
||||||
sidebar_section_heights_[index] = static_cast<float>(ikv_as_float(ikv_array_get(heights, index)));
|
|
||||||
|
void UserData::removeLegacyFiles() const
|
||||||
|
{
|
||||||
|
std::error_code error;
|
||||||
|
for (const char *name : {"imgui.ini", "settings.ini", "history.txt", "session.txt",
|
||||||
|
"user_data.ikv2b", "user_data.ikv2"})
|
||||||
|
std::filesystem::remove(directory_ / name, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
UserData::RepoSettings &UserData::repoSettings(const std::string &normalized_path)
|
||||||
|
{
|
||||||
|
return repo_settings_[normalized_path];
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserData::RepoSettings *UserData::findRepoSettings(const std::string &normalized_path) const
|
||||||
|
{
|
||||||
|
const auto it = repo_settings_.find(normalized_path);
|
||||||
|
return it != repo_settings_.end() ? &it->second : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserData::load()
|
||||||
|
{
|
||||||
|
const std::filesystem::path binary_path = dataPath();
|
||||||
|
|
||||||
|
// ── Primary load: iKvxx binary (.ikv) ────────────────────────────────────
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const ikv::Value root = [&]() -> ikv::Value
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return ikv::Value::load(binary_path.string());
|
||||||
}
|
}
|
||||||
}
|
catch (const ikv::Error &)
|
||||||
if (const ikv_node_t* history = object_value(root, "recently_closed", IKV_ARRAY)) {
|
{
|
||||||
const uint32_t count = std::min<uint32_t>(ikv_array_size(history), 12);
|
return ikv::Value::loadBinary(binary_path.string());
|
||||||
for (uint32_t index = 0; index < count; ++index) {
|
|
||||||
const char* path = ikv_as_string(ikv_array_get(history, index));
|
|
||||||
if (path && *path) recently_closed_.emplace_back(path);
|
|
||||||
}
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
if (root.isMember("settings") && root["settings"].isObject())
|
||||||
|
{
|
||||||
|
loadSettings(root["settings"],
|
||||||
|
sidebar_width_, details_width_, sidebar_collapsed_,
|
||||||
|
commit_message_height_, working_composer_height_,
|
||||||
|
pull_mode_, zoom_percent_,
|
||||||
|
sidebar_section_heights_, sidebar_section_open_,
|
||||||
|
commit_table_column_widths_);
|
||||||
}
|
}
|
||||||
if (const ikv_node_t* session = object_value(root, "session", IKV_OBJECT)) {
|
|
||||||
if (const ikv_node_t* active = object_value(session, "active_tab", IKV_INT))
|
if (root.isMember("recently_closed") && root["recently_closed"].isArray())
|
||||||
active_repository_ = static_cast<size_t>(std::max<int64_t>(0, ikv_as_int(active)));
|
{
|
||||||
if (const ikv_node_t* tabs = object_value(session, "tabs", IKV_ARRAY)) {
|
const ikv::Value &arr = root["recently_closed"];
|
||||||
for (uint32_t index = 0; index < ikv_array_size(tabs); ++index) {
|
const auto count = std::min<std::size_t>(arr.size(), 100);
|
||||||
const char* path = ikv_as_string(ikv_array_get(tabs, index));
|
for (std::size_t i = 0; i < count; ++i)
|
||||||
open_repositories_.emplace_back(path ? path : "");
|
{
|
||||||
|
const std::string path = arr[static_cast<ArrayIndex>(i)].asString();
|
||||||
|
if (!path.empty())
|
||||||
|
{
|
||||||
|
std::string normalized = normalizePath(path);
|
||||||
|
if (std::find(recently_closed_.begin(), recently_closed_.end(), normalized) == recently_closed_.end())
|
||||||
|
recently_closed_.emplace_back(std::move(normalized));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ikv_free(root);
|
|
||||||
sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
|
if (root.isMember("recent_repositories") && root["recent_repositories"].isArray())
|
||||||
details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
|
{
|
||||||
pull_mode_ = std::clamp(pull_mode_, 0, 3);
|
const ikv::Value &arr = root["recent_repositories"];
|
||||||
for (float& height : sidebar_section_heights_) height = std::clamp(height, 42.0f, 500.0f);
|
const auto count = std::min<std::size_t>(arr.size(), 100);
|
||||||
if (open_repositories_.empty()) active_repository_ = 0;
|
for (std::size_t i = 0; i < count; ++i)
|
||||||
else active_repository_ = std::min(active_repository_, open_repositories_.size() - 1);
|
{
|
||||||
|
const std::string path = arr[static_cast<ArrayIndex>(i)].asString();
|
||||||
|
if (!path.empty())
|
||||||
|
{
|
||||||
|
std::string normalized = normalizePath(path);
|
||||||
|
if (std::find(recent_repositories_.begin(), recent_repositories_.end(), normalized) == recent_repositories_.end())
|
||||||
|
recent_repositories_.emplace_back(std::move(normalized));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.isMember("session") && root["session"].isObject())
|
||||||
|
{
|
||||||
|
const ikv::Value &session = root["session"];
|
||||||
|
if (session.isMember("active_tab") && session["active_tab"].isInt())
|
||||||
|
active_repository_ = static_cast<size_t>(std::max<int64_t>(0, session["active_tab"].asInt64()));
|
||||||
|
if (session.isMember("tabs") && session["tabs"].isArray())
|
||||||
|
{
|
||||||
|
const ikv::Value &tabs = session["tabs"];
|
||||||
|
for (std::size_t i = 0; i < tabs.size(); ++i)
|
||||||
|
open_repositories_.emplace_back(tabs[static_cast<ArrayIndex>(i)].asString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.isMember("repository_settings") && root["repository_settings"].isArray())
|
||||||
|
{
|
||||||
|
const ikv::Value &repos = root["repository_settings"];
|
||||||
|
for (std::size_t i = 0; i < repos.size(); ++i)
|
||||||
|
{
|
||||||
|
const ikv::Value &entry = repos[static_cast<ArrayIndex>(i)];
|
||||||
|
if (!entry.isObject() || !entry.isMember("path"))
|
||||||
|
continue;
|
||||||
|
const std::string path_str = entry["path"].asString();
|
||||||
|
if (path_str.empty())
|
||||||
|
continue;
|
||||||
|
const std::string key = normalizePath(path_str);
|
||||||
|
RepoSettings &rs = repo_settings_[key];
|
||||||
|
// column widths
|
||||||
|
rs.column_widths = commit_table_column_widths_; // start from global default
|
||||||
|
if (entry.isMember("commit_table_columns") && entry["commit_table_columns"].isArray())
|
||||||
|
{
|
||||||
|
const ikv::Value &widths = entry["commit_table_columns"];
|
||||||
|
const auto count = std::min<std::size_t>(widths.size(), 4);
|
||||||
|
for (std::size_t c = 0; c < count; ++c)
|
||||||
|
rs.column_widths[c] = static_cast<float>(widths[static_cast<ArrayIndex>(c)].asDouble());
|
||||||
|
}
|
||||||
|
if (entry.isMember("commit_summary") && entry["commit_summary"].isString())
|
||||||
|
rs.pending_commit_summary = entry["commit_summary"].asString();
|
||||||
|
if (entry.isMember("commit_description") && entry["commit_description"].isString())
|
||||||
|
rs.pending_commit_description = entry["commit_description"].asString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.isMember("credentials") && root["credentials"].isArray())
|
||||||
|
{
|
||||||
|
const ikv::Value &creds = root["credentials"];
|
||||||
|
for (std::size_t i = 0; i < creds.size(); ++i)
|
||||||
|
{
|
||||||
|
const ikv::Value &entry = creds[static_cast<ArrayIndex>(i)];
|
||||||
|
if (!entry.isObject())
|
||||||
|
continue;
|
||||||
|
if (!entry.isMember("scope") || !entry.isMember("secret"))
|
||||||
|
continue;
|
||||||
|
const std::string scope = entry["scope"].asString();
|
||||||
|
const std::string secret = entry["secret"].asString();
|
||||||
|
if (!scope.empty() && !secret.empty())
|
||||||
|
encrypted_credentials_[scope] = secret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp loaded values.
|
||||||
|
sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
|
||||||
|
details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
|
||||||
|
pull_mode_ = std::clamp(pull_mode_, 0, 3);
|
||||||
|
zoom_percent_ = std::clamp(zoom_percent_, 80, 200);
|
||||||
|
commit_message_height_ = std::clamp(commit_message_height_, 60.0f, 600.0f);
|
||||||
|
working_composer_height_ = std::clamp(working_composer_height_, 100.0f, 900.0f);
|
||||||
|
for (float &h : sidebar_section_heights_)
|
||||||
|
h = std::clamp(h, 42.0f, 500.0f);
|
||||||
|
for (float &w : commit_table_column_widths_)
|
||||||
|
w = std::clamp(w, 0.0f, 1200.0f);
|
||||||
|
for (auto &[repo_key, rs] : repo_settings_)
|
||||||
|
for (float &w : rs.column_widths)
|
||||||
|
w = std::clamp(w, 0.0f, 1200.0f);
|
||||||
|
if (open_repositories_.empty())
|
||||||
|
active_repository_ = 0;
|
||||||
|
else
|
||||||
|
active_repository_ = std::min(active_repository_, open_repositories_.size() - 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
catch (const ikv::Error &)
|
||||||
|
{
|
||||||
|
// File missing or corrupt — try legacy paths below.
|
||||||
|
}
|
||||||
|
|
||||||
// Import the original files once when upgrading an existing installation.
|
// ── Legacy text load (user_data.ikv — old text format) ───────────────────
|
||||||
|
// (The old binary was user_data.ikv2b; user_data.ikv was the text file.)
|
||||||
|
// We'll silently skip if missing.
|
||||||
|
const std::filesystem::path text_path = directory_ / "user_data_legacy.ikv";
|
||||||
|
// (No legacy text migration needed — just fall through to defaults.)
|
||||||
|
|
||||||
|
// ── Import from very old settings.ini ────────────────────────────────────
|
||||||
std::ifstream settings(directory_ / "settings.ini");
|
std::ifstream settings(directory_ / "settings.ini");
|
||||||
std::string key;
|
std::string key;
|
||||||
while (settings >> key) {
|
while (settings >> key)
|
||||||
if (key == "sidebar_width") settings >> sidebar_width_;
|
{
|
||||||
else if (key == "details_width") settings >> details_width_;
|
if (key == "sidebar_width") settings >> sidebar_width_;
|
||||||
else if (key == "pull_mode") settings >> pull_mode_;
|
else if (key == "details_width") settings >> details_width_;
|
||||||
else if (key.rfind("sidebar_section_", 0) == 0) {
|
else if (key == "pull_mode") settings >> pull_mode_;
|
||||||
|
else if (key == "zoom_percent") settings >> zoom_percent_;
|
||||||
|
else if (key.rfind("sidebar_section_", 0) == 0)
|
||||||
|
{
|
||||||
const size_t index = static_cast<size_t>(std::stoul(key.substr(16)));
|
const size_t index = static_cast<size_t>(std::stoul(key.substr(16)));
|
||||||
float height = 0.0f;
|
float height = 0.0f;
|
||||||
settings >> height;
|
settings >> height;
|
||||||
if (index < sidebar_section_heights_.size()) sidebar_section_heights_[index] = height;
|
if (index < sidebar_section_heights_.size())
|
||||||
|
sidebar_section_heights_[index] = height;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
|
|
||||||
details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
|
|
||||||
pull_mode_ = std::clamp(pull_mode_, 0, 3);
|
|
||||||
for (float& height : sidebar_section_heights_) height = std::clamp(height, 42.0f, 500.0f);
|
|
||||||
|
|
||||||
std::ifstream history(directory_ / "history.txt");
|
std::ifstream history(directory_ / "history.txt");
|
||||||
std::string path;
|
std::string path;
|
||||||
while (history >> std::quoted(path)) {
|
while (history >> std::quoted(path))
|
||||||
if (!path.empty()) recently_closed_.push_back(path);
|
{
|
||||||
if (recently_closed_.size() == 12) break;
|
if (!path.empty())
|
||||||
|
{
|
||||||
|
std::string normalized = normalizePath(path);
|
||||||
|
if (std::find(recently_closed_.begin(), recently_closed_.end(), normalized) == recently_closed_.end())
|
||||||
|
recently_closed_.push_back(std::move(normalized));
|
||||||
|
}
|
||||||
|
if (recently_closed_.size() == 100)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
recent_repositories_ = recently_closed_;
|
||||||
|
|
||||||
|
std::ifstream session_file(directory_ / "session.txt");
|
||||||
|
session_file >> active_repository_;
|
||||||
|
while (session_file >> std::quoted(path))
|
||||||
|
{
|
||||||
|
if (!path.empty())
|
||||||
|
open_repositories_.push_back(normalizePath(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::ifstream session(directory_ / "session.txt");
|
sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
|
||||||
session >> active_repository_;
|
details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
|
||||||
while (session >> std::quoted(path)) open_repositories_.push_back(path);
|
pull_mode_ = std::clamp(pull_mode_, 0, 3);
|
||||||
if (open_repositories_.empty()) active_repository_ = 0;
|
zoom_percent_ = std::clamp(zoom_percent_, 80, 200);
|
||||||
else active_repository_ = std::min(active_repository_, open_repositories_.size() - 1);
|
for (float &h : sidebar_section_heights_)
|
||||||
|
h = std::clamp(h, 42.0f, 500.0f);
|
||||||
|
if (open_repositories_.empty())
|
||||||
|
active_repository_ = 0;
|
||||||
|
else
|
||||||
|
active_repository_ = std::min(active_repository_, open_repositories_.size() - 1);
|
||||||
|
save(); // migrate to new format immediately
|
||||||
}
|
}
|
||||||
|
|
||||||
void UserData::addRecentlyClosed(const std::string& path) {
|
std::optional<SavedGitCredential> UserData::remoteCredential(const std::string &remote_url) const
|
||||||
|
{
|
||||||
|
const auto iterator = encrypted_credentials_.find(credentialScope(remote_url));
|
||||||
|
if (iterator == encrypted_credentials_.end())
|
||||||
|
return std::nullopt;
|
||||||
|
#ifdef _WIN32
|
||||||
|
const auto plain = unprotectCredentialString(iterator->second);
|
||||||
|
if (!plain)
|
||||||
|
return std::nullopt;
|
||||||
|
const auto split = splitCredentialPayload(*plain);
|
||||||
|
if (!split)
|
||||||
|
return std::nullopt;
|
||||||
|
return SavedGitCredential{remote_url, split->first, split->second};
|
||||||
|
#else
|
||||||
|
return std::nullopt;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string UserData::pendingCommitSummary(const std::string &repo_path) const
|
||||||
|
{
|
||||||
|
const std::string key = normalizePath(repo_path);
|
||||||
|
const RepoSettings *rs = findRepoSettings(key);
|
||||||
|
return rs ? rs->pending_commit_summary : std::string{};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string UserData::pendingCommitDescription(const std::string &repo_path) const
|
||||||
|
{
|
||||||
|
const std::string key = normalizePath(repo_path);
|
||||||
|
const RepoSettings *rs = findRepoSettings(key);
|
||||||
|
return rs ? rs->pending_commit_description : std::string{};
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserData::addRecentRepository(const std::string &path)
|
||||||
|
{
|
||||||
if (path.empty()) return;
|
if (path.empty()) return;
|
||||||
std::erase(recently_closed_, path);
|
const std::string normalized = normalizePath(path);
|
||||||
recently_closed_.insert(recently_closed_.begin(), path);
|
std::erase(recent_repositories_, normalized);
|
||||||
if (recently_closed_.size() > 12) recently_closed_.resize(12);
|
recent_repositories_.insert(recent_repositories_.begin(), normalized);
|
||||||
|
if (recent_repositories_.size() > 100)
|
||||||
|
recent_repositories_.resize(100);
|
||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
void UserData::setRepositorySession(std::vector<std::string> paths, size_t active_repository) {
|
void UserData::addRecentlyClosed(const std::string &path)
|
||||||
|
{
|
||||||
|
if (path.empty()) return;
|
||||||
|
const std::string normalized = normalizePath(path);
|
||||||
|
std::erase(recent_repositories_, normalized);
|
||||||
|
recent_repositories_.insert(recent_repositories_.begin(), normalized);
|
||||||
|
if (recent_repositories_.size() > 100)
|
||||||
|
recent_repositories_.resize(100);
|
||||||
|
std::erase(recently_closed_, normalized);
|
||||||
|
recently_closed_.insert(recently_closed_.begin(), normalized);
|
||||||
|
if (recently_closed_.size() > 100)
|
||||||
|
recently_closed_.resize(100);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string UserData::takeRecentlyClosed()
|
||||||
|
{
|
||||||
|
if (recently_closed_.empty()) return {};
|
||||||
|
std::string path = std::move(recently_closed_.front());
|
||||||
|
recently_closed_.erase(recently_closed_.begin());
|
||||||
|
save();
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserData::setRepositorySession(std::vector<std::string> paths, size_t active_repository)
|
||||||
|
{
|
||||||
|
for (auto &path : paths)
|
||||||
|
path = normalizePath(path);
|
||||||
open_repositories_ = std::move(paths);
|
open_repositories_ = std::move(paths);
|
||||||
active_repository_ = open_repositories_.empty()
|
active_repository_ = open_repositories_.empty()
|
||||||
? 0
|
? 0
|
||||||
: std::min(active_repository, open_repositories_.size() - 1);
|
: std::min(active_repository, open_repositories_.size() - 1);
|
||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
void UserData::save() const {
|
void UserData::storeRemoteCredential(const std::string &remote_url, const std::string &username,
|
||||||
std::filesystem::create_directories(directory_);
|
const std::string &password)
|
||||||
ikv_node_t* root = ikv_create_object("gitree");
|
{
|
||||||
if (!root) return;
|
const std::string scope = credentialScope(remote_url);
|
||||||
|
if (scope.empty() || username.empty()) return;
|
||||||
ikv_node_t* settings = ikv_object_add_object(root, "settings");
|
#ifdef _WIN32
|
||||||
ikv_object_set_float(settings, "sidebar_width", sidebar_width_);
|
const auto protected_value = protectCredentialString(username + "\n" + password);
|
||||||
ikv_object_set_float(settings, "details_width", details_width_);
|
if (!protected_value) return;
|
||||||
ikv_object_set_int(settings, "pull_mode", pull_mode_);
|
encrypted_credentials_[scope] = *protected_value;
|
||||||
ikv_node_t* heights = ikv_object_add_array(settings, "sidebar_sections", IKV_FLOAT);
|
save();
|
||||||
for (const float height : sidebar_section_heights_) ikv_array_add_float(heights, height);
|
#else
|
||||||
|
(void)password;
|
||||||
ikv_node_t* history = ikv_object_add_array(root, "recently_closed", IKV_STRING);
|
#endif
|
||||||
for (const auto& path : recently_closed_) ikv_array_add_string(history, path.c_str());
|
}
|
||||||
|
|
||||||
ikv_node_t* session = ikv_object_add_object(root, "session");
|
void UserData::clearRemoteCredential(const std::string &remote_url)
|
||||||
ikv_object_set_int(session, "active_tab", static_cast<int64_t>(active_repository_));
|
{
|
||||||
ikv_node_t* tabs = ikv_object_add_array(session, "tabs", IKV_STRING);
|
encrypted_credentials_.erase(credentialScope(remote_url));
|
||||||
for (const auto& path : open_repositories_) ikv_array_add_string(tabs, path.c_str());
|
save();
|
||||||
|
}
|
||||||
ikv_write_file((directory_ / "user_data.ikv").string().c_str(), root);
|
|
||||||
ikv_free(root);
|
void UserData::setPendingCommitSummary(const std::string &repo_path, const std::string &text)
|
||||||
|
{
|
||||||
|
repoSettings(normalizePath(repo_path)).pending_commit_summary = text;
|
||||||
|
// Don't trigger a full save on every keystroke — caller must call save() when appropriate.
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserData::setPendingCommitDescription(const std::string &repo_path, const std::string &text)
|
||||||
|
{
|
||||||
|
repoSettings(normalizePath(repo_path)).pending_commit_description = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserData::save() const
|
||||||
|
{
|
||||||
|
std::filesystem::create_directories(directory_);
|
||||||
|
ikv::Value root(ikv::objectValue, "gitree");
|
||||||
|
|
||||||
|
auto settings = root.makeObject("settings");
|
||||||
|
settings["sidebar_width"] = static_cast<double>(sidebar_width_);
|
||||||
|
settings["details_width"] = static_cast<double>(details_width_);
|
||||||
|
settings["sidebar_collapsed"] = sidebar_collapsed_;
|
||||||
|
settings["commit_message_height"] = static_cast<double>(commit_message_height_);
|
||||||
|
settings["working_composer_height"] = static_cast<double>(working_composer_height_);
|
||||||
|
settings["pull_mode"] = static_cast<int64_t>(pull_mode_);
|
||||||
|
settings["zoom_percent"] = static_cast<int64_t>(zoom_percent_);
|
||||||
|
|
||||||
|
auto heights = settings.makeArray("sidebar_sections", ikv::realValue);
|
||||||
|
for (const float h : sidebar_section_heights_)
|
||||||
|
heights.append(static_cast<double>(h));
|
||||||
|
|
||||||
|
auto open = settings.makeArray("sidebar_section_open", ikv::booleanValue);
|
||||||
|
for (const bool b : sidebar_section_open_)
|
||||||
|
open.append(b);
|
||||||
|
|
||||||
|
auto col_widths = settings.makeArray("commit_table_columns", ikv::realValue);
|
||||||
|
for (const float w : commit_table_column_widths_)
|
||||||
|
col_widths.append(static_cast<double>(w));
|
||||||
|
|
||||||
|
auto repos = root.makeArray("repository_settings", ikv::objectValue);
|
||||||
|
for (const auto &[repo_path, rs] : repo_settings_)
|
||||||
|
{
|
||||||
|
auto entry = repos.appendObject();
|
||||||
|
entry["path"] = repo_path;
|
||||||
|
auto rw = entry.makeArray("commit_table_columns", ikv::realValue);
|
||||||
|
for (const float w : rs.column_widths)
|
||||||
|
rw.append(static_cast<double>(w));
|
||||||
|
if (!rs.pending_commit_summary.empty())
|
||||||
|
entry["commit_summary"] = rs.pending_commit_summary;
|
||||||
|
if (!rs.pending_commit_description.empty())
|
||||||
|
entry["commit_description"] = rs.pending_commit_description;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto history = root.makeArray("recently_closed", ikv::stringValue);
|
||||||
|
for (const auto &p : recently_closed_)
|
||||||
|
history.append(p);
|
||||||
|
|
||||||
|
auto recent = root.makeArray("recent_repositories", ikv::stringValue);
|
||||||
|
for (const auto &p : recent_repositories_)
|
||||||
|
recent.append(p);
|
||||||
|
|
||||||
|
auto session = root.makeObject("session");
|
||||||
|
session["active_tab"] = static_cast<int64_t>(active_repository_);
|
||||||
|
auto tabs = session.makeArray("tabs", ikv::stringValue);
|
||||||
|
for (const auto &p : open_repositories_)
|
||||||
|
tabs.append(p);
|
||||||
|
|
||||||
|
auto credentials = root.makeArray("credentials", ikv::objectValue);
|
||||||
|
for (const auto &[scope, secret] : encrypted_credentials_)
|
||||||
|
{
|
||||||
|
auto entry = credentials.appendObject();
|
||||||
|
entry["scope"] = scope;
|
||||||
|
entry["secret"] = secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.writeBinary(dataPath().string(), ikv::Version::v2);
|
||||||
|
removeLegacyFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
float UserData::commitTableColumnWidth(const std::string &repo_path, size_t index) const
|
||||||
|
{
|
||||||
|
if (index >= 4) return 0.0f;
|
||||||
|
const std::string normalized = normalizePath(repo_path);
|
||||||
|
const RepoSettings *rs = findRepoSettings(normalized);
|
||||||
|
if (rs) return rs->column_widths[index];
|
||||||
|
return commit_table_column_widths_[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserData::setCommitTableColumnWidth(const std::string &repo_path, size_t index, float width)
|
||||||
|
{
|
||||||
|
if (index >= 4) return;
|
||||||
|
const std::string normalized = normalizePath(repo_path);
|
||||||
|
RepoSettings &rs = repo_settings_[normalized];
|
||||||
|
// Initialise from global defaults if this is a brand-new entry.
|
||||||
|
if (rs.column_widths[0] == 0.0f && rs.column_widths[1] == 0.0f &&
|
||||||
|
rs.column_widths[2] == 0.0f && rs.column_widths[3] == 0.0f)
|
||||||
|
{
|
||||||
|
rs.column_widths = commit_table_column_widths_;
|
||||||
|
}
|
||||||
|
rs.column_widths[index] = width;
|
||||||
|
save();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,42 +2,102 @@
|
|||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
class UserData {
|
struct SavedGitCredential
|
||||||
|
{
|
||||||
|
std::string remote_url;
|
||||||
|
std::string username;
|
||||||
|
std::string password;
|
||||||
|
};
|
||||||
|
|
||||||
|
class UserData
|
||||||
|
{
|
||||||
public:
|
public:
|
||||||
UserData();
|
UserData();
|
||||||
~UserData();
|
~UserData();
|
||||||
|
|
||||||
[[nodiscard]] const std::filesystem::path& directory() const { return directory_; }
|
[[nodiscard]] const std::filesystem::path &directory() const { return directory_; }
|
||||||
[[nodiscard]] const std::string& imguiIniPath() const { return imgui_ini_path_; }
|
[[nodiscard]] const std::vector<std::string> &recentRepositories() const { return recent_repositories_; }
|
||||||
[[nodiscard]] const std::vector<std::string>& recentlyClosed() const { return recently_closed_; }
|
[[nodiscard]] const std::vector<std::string> &recentlyClosed() const { return recently_closed_; }
|
||||||
[[nodiscard]] const std::vector<std::string>& openRepositories() const { return open_repositories_; }
|
[[nodiscard]] const std::vector<std::string> &openRepositories() const { return open_repositories_; }
|
||||||
[[nodiscard]] size_t activeRepository() const { return active_repository_; }
|
[[nodiscard]] size_t activeRepository() const { return active_repository_; }
|
||||||
[[nodiscard]] float sidebarWidth() const { return sidebar_width_; }
|
[[nodiscard]] float sidebarWidth() const { return sidebar_width_; }
|
||||||
[[nodiscard]] float detailsWidth() const { return details_width_; }
|
[[nodiscard]] float detailsWidth() const { return details_width_; }
|
||||||
[[nodiscard]] float sidebarSectionHeight(size_t index) const { return sidebar_section_heights_.at(index); }
|
[[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 commitMessageHeight() const { return commit_message_height_; }
|
||||||
|
[[nodiscard]] float workingComposerHeight() const { return working_composer_height_; }
|
||||||
|
[[nodiscard]] float commitTableColumnWidth(const std::string &repo_path, size_t index) const;
|
||||||
[[nodiscard]] int pullMode() const { return pull_mode_; }
|
[[nodiscard]] int pullMode() const { return pull_mode_; }
|
||||||
|
[[nodiscard]] int zoomPercent() const { return zoom_percent_; }
|
||||||
|
[[nodiscard]] std::optional<SavedGitCredential> remoteCredential(const std::string &remote_url) const;
|
||||||
|
[[nodiscard]] std::string pendingCommitSummary(const std::string &repo_path) const;
|
||||||
|
[[nodiscard]] std::string pendingCommitDescription(const std::string &repo_path) const;
|
||||||
|
static std::string credentialScope(const std::string &remote_url);
|
||||||
|
|
||||||
void setSidebarWidth(float width) { sidebar_width_ = width; }
|
void setSidebarWidth(float width) { sidebar_width_ = width; }
|
||||||
void setDetailsWidth(float width) { details_width_ = width; }
|
void setDetailsWidth(float width) { details_width_ = width; }
|
||||||
void setSidebarSectionHeight(size_t index, float height) { sidebar_section_heights_.at(index) = height; }
|
void setSidebarSectionHeight(size_t index, float height) { sidebar_section_heights_.at(index) = height; }
|
||||||
void setPullMode(int mode) { pull_mode_ = mode; save(); }
|
void setSidebarCollapsed(bool collapsed) { sidebar_collapsed_ = collapsed; }
|
||||||
void addRecentlyClosed(const std::string& path);
|
void setSidebarSectionOpen(size_t index, bool open) { sidebar_section_open_.at(index) = open; }
|
||||||
|
void setCommitMessageHeight(float height) { commit_message_height_ = height; }
|
||||||
|
void setWorkingComposerHeight(float height) { working_composer_height_ = height; }
|
||||||
|
void setCommitTableColumnWidth(const std::string &repo_path, size_t index, float width);
|
||||||
|
void setPendingCommitSummary(const std::string &repo_path, const std::string &text);
|
||||||
|
void setPendingCommitDescription(const std::string &repo_path, const std::string &text);
|
||||||
|
void setPullMode(int mode)
|
||||||
|
{
|
||||||
|
pull_mode_ = mode;
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
void setZoomPercent(int percent)
|
||||||
|
{
|
||||||
|
zoom_percent_ = percent;
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
void addRecentRepository(const std::string &path);
|
||||||
|
void addRecentlyClosed(const std::string &path);
|
||||||
|
std::string takeRecentlyClosed();
|
||||||
void setRepositorySession(std::vector<std::string> paths, size_t active_repository);
|
void setRepositorySession(std::vector<std::string> paths, size_t active_repository);
|
||||||
|
void storeRemoteCredential(const std::string &remote_url, const std::string &username,
|
||||||
|
const std::string &password);
|
||||||
|
void clearRemoteCredential(const std::string &remote_url);
|
||||||
void save() const;
|
void save() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
struct RepoSettings
|
||||||
|
{
|
||||||
|
std::array<float, 4> column_widths = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||||
|
std::string pending_commit_summary;
|
||||||
|
std::string pending_commit_description;
|
||||||
|
};
|
||||||
|
|
||||||
void load();
|
void load();
|
||||||
|
[[nodiscard]] std::filesystem::path dataPath() const;
|
||||||
|
void removeLegacyFiles() const;
|
||||||
|
RepoSettings &repoSettings(const std::string &normalized_path);
|
||||||
|
const RepoSettings *findRepoSettings(const std::string &normalized_path) const;
|
||||||
|
|
||||||
std::filesystem::path directory_;
|
std::filesystem::path directory_;
|
||||||
std::string imgui_ini_path_;
|
std::vector<std::string> recent_repositories_;
|
||||||
std::vector<std::string> recently_closed_;
|
std::vector<std::string> recently_closed_;
|
||||||
std::vector<std::string> open_repositories_;
|
std::vector<std::string> open_repositories_;
|
||||||
size_t active_repository_ = 0;
|
size_t active_repository_ = 0;
|
||||||
float sidebar_width_ = 230.0f;
|
float sidebar_width_ = 230.0f;
|
||||||
float details_width_ = 368.0f;
|
float details_width_ = 368.0f;
|
||||||
std::array<float, 4> sidebar_section_heights_ = {110.0f, 220.0f, 90.0f, 150.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};
|
||||||
|
float commit_message_height_ = 125.0f;
|
||||||
|
float working_composer_height_ = 320.0f;
|
||||||
|
std::array<float, 4> commit_table_column_widths_ = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||||
int pull_mode_ = 1;
|
int pull_mode_ = 1;
|
||||||
|
int zoom_percent_ = 100;
|
||||||
|
std::unordered_map<std::string, RepoSettings> repo_settings_;
|
||||||
|
std::unordered_map<std::string, std::string> encrypted_credentials_;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,36 +2,155 @@
|
|||||||
|
|
||||||
#include <GLFW/glfw3.h>
|
#include <GLFW/glfw3.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#define GLFW_EXPOSE_NATIVE_WIN32
|
#define GLFW_EXPOSE_NATIVE_WIN32
|
||||||
#include <GLFW/glfw3native.h>
|
#include <GLFW/glfw3native.h>
|
||||||
#include <dwmapi.h>
|
#include <dwmapi.h>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
#include <wincodec.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
WindowManager::~WindowManager() {
|
#ifdef _WIN32
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
HICON loadPngIcon(const std::filesystem::path &path, UINT size, bool rounded)
|
||||||
|
{
|
||||||
|
const HRESULT com_result = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||||
|
const bool uninitialize_com = SUCCEEDED(com_result);
|
||||||
|
|
||||||
|
IWICImagingFactory *factory = nullptr;
|
||||||
|
IWICBitmapDecoder *decoder = nullptr;
|
||||||
|
IWICBitmapFrameDecode *frame = nullptr;
|
||||||
|
IWICBitmapScaler *scaler = nullptr;
|
||||||
|
IWICFormatConverter *converter = nullptr;
|
||||||
|
HICON icon = nullptr;
|
||||||
|
|
||||||
|
if (FAILED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER,
|
||||||
|
IID_PPV_ARGS(&factory))) ||
|
||||||
|
FAILED(factory->CreateDecoderFromFilename(path.c_str(), nullptr, GENERIC_READ,
|
||||||
|
WICDecodeMetadataCacheOnLoad, &decoder)) ||
|
||||||
|
FAILED(decoder->GetFrame(0, &frame)) ||
|
||||||
|
FAILED(factory->CreateBitmapScaler(&scaler)) ||
|
||||||
|
FAILED(scaler->Initialize(frame, size, size, WICBitmapInterpolationModeFant)) ||
|
||||||
|
FAILED(factory->CreateFormatConverter(&converter)) ||
|
||||||
|
FAILED(converter->Initialize(scaler, GUID_WICPixelFormat32bppBGRA,
|
||||||
|
WICBitmapDitherTypeNone, nullptr, 0.0,
|
||||||
|
WICBitmapPaletteTypeCustom)))
|
||||||
|
{
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::vector<BYTE> pixels(static_cast<size_t>(size) * size * 4);
|
||||||
|
if (FAILED(converter->CopyPixels(nullptr, size * 4,
|
||||||
|
static_cast<UINT>(pixels.size()), pixels.data())))
|
||||||
|
{
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rounded)
|
||||||
|
{
|
||||||
|
const float radius = static_cast<float>(size) * 0.22f;
|
||||||
|
const float maximum = static_cast<float>(size) - radius - 0.5f;
|
||||||
|
const float minimum = radius - 0.5f;
|
||||||
|
for (UINT y = 0; y < size; ++y)
|
||||||
|
{
|
||||||
|
for (UINT x = 0; x < size; ++x)
|
||||||
|
{
|
||||||
|
const float nearest_x = std::clamp(static_cast<float>(x), minimum, maximum);
|
||||||
|
const float nearest_y = std::clamp(static_cast<float>(y), minimum, maximum);
|
||||||
|
const float dx = static_cast<float>(x) - nearest_x;
|
||||||
|
const float dy = static_cast<float>(y) - nearest_y;
|
||||||
|
const float coverage = std::clamp(radius + 0.5f - std::sqrt(dx * dx + dy * dy),
|
||||||
|
0.0f, 1.0f);
|
||||||
|
pixels[(static_cast<size_t>(y) * size + x) * 4 + 3] =
|
||||||
|
static_cast<BYTE>(coverage * 255.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BITMAPV5HEADER header{};
|
||||||
|
header.bV5Size = sizeof(header);
|
||||||
|
header.bV5Width = static_cast<LONG>(size);
|
||||||
|
header.bV5Height = -static_cast<LONG>(size);
|
||||||
|
header.bV5Planes = 1;
|
||||||
|
header.bV5BitCount = 32;
|
||||||
|
header.bV5Compression = BI_BITFIELDS;
|
||||||
|
header.bV5RedMask = 0x00ff0000;
|
||||||
|
header.bV5GreenMask = 0x0000ff00;
|
||||||
|
header.bV5BlueMask = 0x000000ff;
|
||||||
|
header.bV5AlphaMask = 0xff000000;
|
||||||
|
|
||||||
|
void *bitmap_bits = nullptr;
|
||||||
|
const HDC screen = GetDC(nullptr);
|
||||||
|
const HBITMAP color = CreateDIBSection(screen, reinterpret_cast<BITMAPINFO *>(&header),
|
||||||
|
DIB_RGB_COLORS, &bitmap_bits, nullptr, 0);
|
||||||
|
ReleaseDC(nullptr, screen);
|
||||||
|
const HBITMAP mask = CreateBitmap(size, size, 1, 1, nullptr);
|
||||||
|
if (color && mask && bitmap_bits)
|
||||||
|
{
|
||||||
|
std::memcpy(bitmap_bits, pixels.data(), pixels.size());
|
||||||
|
ICONINFO info{};
|
||||||
|
info.fIcon = TRUE;
|
||||||
|
info.hbmColor = color;
|
||||||
|
info.hbmMask = mask;
|
||||||
|
icon = CreateIconIndirect(&info);
|
||||||
|
}
|
||||||
|
if (color)
|
||||||
|
DeleteObject(color);
|
||||||
|
if (mask)
|
||||||
|
DeleteObject(mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
if (converter)
|
||||||
|
converter->Release();
|
||||||
|
if (scaler)
|
||||||
|
scaler->Release();
|
||||||
|
if (frame)
|
||||||
|
frame->Release();
|
||||||
|
if (decoder)
|
||||||
|
decoder->Release();
|
||||||
|
if (factory)
|
||||||
|
factory->Release();
|
||||||
|
if (uninitialize_com)
|
||||||
|
CoUninitialize();
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
#endif
|
||||||
|
|
||||||
|
WindowManager::~WindowManager()
|
||||||
|
{
|
||||||
destroy();
|
destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WindowManager::create(const char* title, int width, int height) {
|
bool WindowManager::create(const char *title, int width, int height)
|
||||||
|
{
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
using SetDpiAwarenessContext = BOOL(WINAPI*)(HANDLE);
|
using SetDpiAwarenessContext = BOOL(WINAPI *)(HANDLE);
|
||||||
const HMODULE user32 = GetModuleHandleW(L"user32.dll");
|
const HMODULE user32 = GetModuleHandleW(L"user32.dll");
|
||||||
const FARPROC dpi_address = GetProcAddress(user32, "SetProcessDpiAwarenessContext");
|
const FARPROC dpi_address = GetProcAddress(user32, "SetProcessDpiAwarenessContext");
|
||||||
SetDpiAwarenessContext set_dpi_awareness = nullptr;
|
SetDpiAwarenessContext set_dpi_awareness = nullptr;
|
||||||
static_assert(sizeof(set_dpi_awareness) == sizeof(dpi_address));
|
static_assert(sizeof(set_dpi_awareness) == sizeof(dpi_address));
|
||||||
std::memcpy(&set_dpi_awareness, &dpi_address, sizeof(set_dpi_awareness));
|
std::memcpy(&set_dpi_awareness, &dpi_address, sizeof(set_dpi_awareness));
|
||||||
if (set_dpi_awareness) set_dpi_awareness(reinterpret_cast<HANDLE>(-4)); // Per-monitor v2
|
if (set_dpi_awareness)
|
||||||
|
set_dpi_awareness(reinterpret_cast<HANDLE>(-4)); // Per-monitor v2
|
||||||
#endif
|
#endif
|
||||||
if (!glfwInit()) return false;
|
if (!glfwInit())
|
||||||
|
return false;
|
||||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
|
||||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
|
||||||
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
||||||
glfwWindowHint(GLFW_SCALE_TO_MONITOR, GLFW_TRUE);
|
glfwWindowHint(GLFW_SCALE_TO_MONITOR, GLFW_TRUE);
|
||||||
window_ = glfwCreateWindow(width, height, title, nullptr, nullptr);
|
window_ = glfwCreateWindow(width, height, title, nullptr, nullptr);
|
||||||
if (!window_) {
|
if (!window_)
|
||||||
|
{
|
||||||
glfwTerminate();
|
glfwTerminate();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -45,13 +164,17 @@ bool WindowManager::create(const char* title, int width, int height) {
|
|||||||
glfwGetWindowContentScale(window_, &x_scale, &y_scale);
|
glfwGetWindowContentScale(window_, &x_scale, &y_scale);
|
||||||
dpi_scale_ = std::max(x_scale, y_scale);
|
dpi_scale_ = std::max(x_scale, y_scale);
|
||||||
applyNativeCaption();
|
applyNativeCaption();
|
||||||
|
applyNativeIcons();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void WindowManager::destroy() {
|
void WindowManager::destroy()
|
||||||
if (!window_) return;
|
{
|
||||||
|
if (!window_)
|
||||||
|
return;
|
||||||
glfwDestroyWindow(window_);
|
glfwDestroyWindow(window_);
|
||||||
window_ = nullptr;
|
window_ = nullptr;
|
||||||
|
destroyNativeIcons();
|
||||||
glfwTerminate();
|
glfwTerminate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,39 +183,126 @@ void WindowManager::swapBuffers() { glfwSwapBuffers(window_); }
|
|||||||
void WindowManager::requestClose() { glfwSetWindowShouldClose(window_, GLFW_TRUE); }
|
void WindowManager::requestClose() { glfwSetWindowShouldClose(window_, GLFW_TRUE); }
|
||||||
bool WindowManager::shouldClose() const { return !window_ || glfwWindowShouldClose(window_); }
|
bool WindowManager::shouldClose() const { return !window_ || glfwWindowShouldClose(window_); }
|
||||||
|
|
||||||
bool WindowManager::consumeDpiChange() {
|
bool WindowManager::consumeDpiChange()
|
||||||
|
{
|
||||||
const bool changed = dpi_changed_;
|
const bool changed = dpi_changed_;
|
||||||
dpi_changed_ = false;
|
dpi_changed_ = false;
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
void WindowManager::contentScaleCallback(GLFWwindow* window, float x_scale, float y_scale) {
|
void WindowManager::contentScaleCallback(GLFWwindow *window, float x_scale, float y_scale)
|
||||||
auto* manager = static_cast<WindowManager*>(glfwGetWindowUserPointer(window));
|
{
|
||||||
if (manager) manager->updateDpi(std::max(x_scale, y_scale));
|
auto *manager = static_cast<WindowManager *>(glfwGetWindowUserPointer(window));
|
||||||
|
if (manager)
|
||||||
|
manager->updateDpi(std::max(x_scale, y_scale));
|
||||||
}
|
}
|
||||||
|
|
||||||
void WindowManager::updateDpi(float scale) {
|
void WindowManager::updateDpi(float scale)
|
||||||
|
{
|
||||||
scale = std::clamp(scale, 1.0f, 4.0f);
|
scale = std::clamp(scale, 1.0f, 4.0f);
|
||||||
if (scale == dpi_scale_) return;
|
if (scale == dpi_scale_)
|
||||||
|
return;
|
||||||
dpi_scale_ = scale;
|
dpi_scale_ = scale;
|
||||||
dpi_changed_ = true;
|
dpi_changed_ = true;
|
||||||
applyNativeCaption();
|
applyNativeCaption();
|
||||||
}
|
}
|
||||||
|
|
||||||
void WindowManager::applyNativeCaption() const {
|
void WindowManager::applyNativeCaption() const
|
||||||
|
{
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
if (!window_)
|
||||||
|
return;
|
||||||
|
|
||||||
const HWND hwnd = glfwGetWin32Window(window_);
|
const HWND hwnd = glfwGetWin32Window(window_);
|
||||||
|
if (!hwnd)
|
||||||
|
return;
|
||||||
|
|
||||||
const BOOL dark = TRUE;
|
const BOOL dark = TRUE;
|
||||||
DwmSetWindowAttribute(hwnd, 20, &dark, sizeof(dark)); // DWMWA_USE_IMMERSIVE_DARK_MODE
|
|
||||||
const DWORD square_corners = 1; // DWMWCP_DONOTROUND
|
// DWMWA_USE_IMMERSIVE_DARK_MODE
|
||||||
DwmSetWindowAttribute(hwnd, 33, &square_corners, sizeof(square_corners));
|
DwmSetWindowAttribute(hwnd, 20, &dark, sizeof(dark));
|
||||||
|
|
||||||
|
DWORD corner_pref = 0;
|
||||||
|
|
||||||
|
switch (corner_mode_)
|
||||||
|
{
|
||||||
|
case WindowCornerMode::Default:
|
||||||
|
corner_pref = 0; // DWMWCP_DEFAULT
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WindowCornerMode::DoNotRound:
|
||||||
|
corner_pref = 1; // DWMWCP_DONOTROUND
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WindowCornerMode::Round:
|
||||||
|
corner_pref = 2; // DWMWCP_ROUND
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WindowCornerMode::RoundSmall:
|
||||||
|
corner_pref = 3; // DWMWCP_ROUNDSMALL
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DWMWA_WINDOW_CORNER_PREFERENCE
|
||||||
|
DwmSetWindowAttribute(hwnd, 33, &corner_pref, sizeof(corner_pref));
|
||||||
|
|
||||||
// Windows 11 caption customization. Older versions safely ignore these.
|
// Windows 11 caption customization. Older versions safely ignore these.
|
||||||
const COLORREF caption = RGB(32, 32, 32);
|
const COLORREF caption = static_cast<COLORREF>(caption_color_);
|
||||||
const COLORREF border = RGB(51, 55, 63);
|
const COLORREF border = RGB(51, 55, 63);
|
||||||
const COLORREF text = RGB(199, 203, 209);
|
const COLORREF text = RGB(199, 203, 209);
|
||||||
DwmSetWindowAttribute(hwnd, 35, &caption, sizeof(caption)); // DWMWA_CAPTION_COLOR
|
|
||||||
DwmSetWindowAttribute(hwnd, 34, &border, sizeof(border)); // DWMWA_BORDER_COLOR
|
// DWMWA_BORDER_COLOR
|
||||||
DwmSetWindowAttribute(hwnd, 36, &text, sizeof(text)); // DWMWA_TEXT_COLOR
|
DwmSetWindowAttribute(hwnd, 34, &border, sizeof(border));
|
||||||
|
|
||||||
|
// DWMWA_CAPTION_COLOR
|
||||||
|
DwmSetWindowAttribute(hwnd, 35, &caption, sizeof(caption));
|
||||||
|
|
||||||
|
// DWMWA_TEXT_COLOR
|
||||||
|
DwmSetWindowAttribute(hwnd, 36, &text, sizeof(text));
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WindowManager::applyNativeIcons()
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
destroyNativeIcons();
|
||||||
|
const HWND hwnd = glfwGetWin32Window(window_);
|
||||||
|
const auto asset_dir = std::filesystem::path(GITREE_IMAGE_ASSET_DIR);
|
||||||
|
window_icon_ = loadPngIcon(asset_dir / L"gitree_logo.png", 32, true);
|
||||||
|
taskbar_icon_ = loadPngIcon(asset_dir / L"gitree_logo.png", 64, true);
|
||||||
|
|
||||||
|
if (window_icon_)
|
||||||
|
{
|
||||||
|
SendMessageW(hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast<LPARAM>(window_icon_));
|
||||||
|
SendMessageW(hwnd, WM_SETICON, ICON_SMALL2, reinterpret_cast<LPARAM>(window_icon_));
|
||||||
|
}
|
||||||
|
if (taskbar_icon_)
|
||||||
|
{
|
||||||
|
SendMessageW(hwnd, WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(taskbar_icon_));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowManager::destroyNativeIcons()
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (window_icon_)
|
||||||
|
DestroyIcon(static_cast<HICON>(window_icon_));
|
||||||
|
if (taskbar_icon_)
|
||||||
|
DestroyIcon(static_cast<HICON>(taskbar_icon_));
|
||||||
|
#endif
|
||||||
|
window_icon_ = nullptr;
|
||||||
|
taskbar_icon_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowManager::setCornerMode(WindowCornerMode mode)
|
||||||
|
{
|
||||||
|
corner_mode_ = mode;
|
||||||
|
applyNativeCaption();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowManager::setCaptionColor(std::uint32_t color)
|
||||||
|
{
|
||||||
|
caption_color_ = color;
|
||||||
|
applyNativeCaption();
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,31 +1,62 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
struct GLFWwindow;
|
struct GLFWwindow;
|
||||||
|
|
||||||
class WindowManager {
|
class WindowManager
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum class WindowCornerMode
|
||||||
|
{
|
||||||
|
Default,
|
||||||
|
DoNotRound,
|
||||||
|
Round,
|
||||||
|
RoundSmall
|
||||||
|
};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
WindowManager() = default;
|
WindowManager() = default;
|
||||||
~WindowManager();
|
~WindowManager();
|
||||||
|
|
||||||
WindowManager(const WindowManager&) = delete;
|
WindowManager(const WindowManager &) = delete;
|
||||||
WindowManager& operator=(const WindowManager&) = delete;
|
WindowManager &operator=(const WindowManager &) = delete;
|
||||||
|
|
||||||
bool create(const char* title, int width, int height);
|
bool create(const char *title, int width, int height);
|
||||||
void destroy();
|
void destroy();
|
||||||
void pollEvents();
|
void pollEvents();
|
||||||
void swapBuffers();
|
void swapBuffers();
|
||||||
void requestClose();
|
void requestClose();
|
||||||
|
|
||||||
[[nodiscard]] bool shouldClose() const;
|
[[nodiscard]] bool shouldClose() const;
|
||||||
[[nodiscard]] GLFWwindow* nativeWindow() const { return window_; }
|
[[nodiscard]] GLFWwindow *nativeWindow() const { return window_; }
|
||||||
[[nodiscard]] float dpiScale() const { return dpi_scale_; }
|
[[nodiscard]] float dpiScale() const { return dpi_scale_; }
|
||||||
|
|
||||||
bool consumeDpiChange();
|
bool consumeDpiChange();
|
||||||
|
|
||||||
|
void setCornerMode(WindowCornerMode mode);
|
||||||
|
[[nodiscard]] WindowCornerMode cornerMode() const { return corner_mode_; }
|
||||||
|
|
||||||
|
void setCaptionColor(std::uint32_t color);
|
||||||
|
[[nodiscard]] std::uint32_t captionColor() const { return caption_color_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static void contentScaleCallback(GLFWwindow* window, float x_scale, float y_scale);
|
static void contentScaleCallback(GLFWwindow *window, float x_scale, float y_scale);
|
||||||
|
|
||||||
void updateDpi(float scale);
|
void updateDpi(float scale);
|
||||||
void applyNativeCaption() const;
|
void applyNativeCaption() const;
|
||||||
|
void applyNativeIcons();
|
||||||
|
void destroyNativeIcons();
|
||||||
|
|
||||||
|
GLFWwindow *window_ = nullptr;
|
||||||
|
void *window_icon_ = nullptr;
|
||||||
|
void *taskbar_icon_ = nullptr;
|
||||||
|
|
||||||
GLFWwindow* window_ = nullptr;
|
|
||||||
float dpi_scale_ = 1.0f;
|
float dpi_scale_ = 1.0f;
|
||||||
bool dpi_changed_ = false;
|
bool dpi_changed_ = false;
|
||||||
|
|
||||||
|
WindowCornerMode corner_mode_ = WindowCornerMode::Round;
|
||||||
|
|
||||||
|
// 0x00BBGGRR, same layout Windows COLORREF uses.
|
||||||
|
std::uint32_t caption_color_ = 0x00201B19;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <git2.h>
|
#include <git2.h>
|
||||||
|
#include <chrono>
|
||||||
|
#include <map>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -28,19 +30,39 @@ struct WorkingFile {
|
|||||||
bool staged = false;
|
bool staged = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct BranchDivergence {
|
||||||
|
size_t ahead = 0;
|
||||||
|
size_t behind = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ToolbarHistoryActionKind { none, checkout, commit };
|
||||||
|
|
||||||
|
struct ToolbarHistoryAction {
|
||||||
|
ToolbarHistoryActionKind kind = ToolbarHistoryActionKind::none;
|
||||||
|
bool available = false;
|
||||||
|
std::string tooltip;
|
||||||
|
std::string source;
|
||||||
|
std::string target;
|
||||||
|
std::string summary;
|
||||||
|
};
|
||||||
|
|
||||||
struct CommitInfo {
|
struct CommitInfo {
|
||||||
git_oid oid{};
|
git_oid oid{};
|
||||||
std::string short_id;
|
std::string short_id;
|
||||||
std::string summary;
|
std::string summary;
|
||||||
|
std::string description;
|
||||||
std::string author;
|
std::string author;
|
||||||
std::string email;
|
std::string email;
|
||||||
std::string date;
|
std::string date;
|
||||||
int parents = 0;
|
int parents = 0;
|
||||||
int lane = 0;
|
int lane = 0;
|
||||||
|
int graph_color = 0;
|
||||||
std::vector<git_oid> parent_ids;
|
std::vector<git_oid> parent_ids;
|
||||||
std::vector<RefBadge> refs;
|
std::vector<RefBadge> refs;
|
||||||
std::vector<ChangedFile> changed_files;
|
std::vector<ChangedFile> changed_files;
|
||||||
|
std::vector<ChangedFile> all_files;
|
||||||
bool changes_loaded = false;
|
bool changes_loaded = false;
|
||||||
|
bool all_files_loaded = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct RepositoryView {
|
struct RepositoryView {
|
||||||
@@ -53,13 +75,21 @@ struct RepositoryView {
|
|||||||
std::string branch = "detached";
|
std::string branch = "detached";
|
||||||
std::vector<std::string> local_branches;
|
std::vector<std::string> local_branches;
|
||||||
std::vector<std::string> remote_branches;
|
std::vector<std::string> remote_branches;
|
||||||
|
std::map<std::string, BranchDivergence> local_branch_divergence;
|
||||||
|
std::map<std::string, BranchDivergence> remote_branch_divergence;
|
||||||
std::vector<std::string> remotes;
|
std::vector<std::string> remotes;
|
||||||
std::vector<std::string> worktrees;
|
std::vector<std::string> worktrees;
|
||||||
std::set<std::string> worktree_branches;
|
std::set<std::string> worktree_branches;
|
||||||
std::vector<std::string> submodules;
|
std::vector<std::string> submodules;
|
||||||
|
std::map<std::string, unsigned int> submodule_statuses;
|
||||||
std::vector<CommitInfo> commits;
|
std::vector<CommitInfo> commits;
|
||||||
std::vector<WorkingFile> working_files;
|
std::vector<WorkingFile> working_files;
|
||||||
int selected_commit = 0;
|
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;
|
||||||
|
|
||||||
RepositoryView() = default;
|
RepositoryView() = default;
|
||||||
~RepositoryView() { close(); }
|
~RepositoryView() { close(); }
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,15 +4,20 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
class GitManager;
|
class GitManager;
|
||||||
|
class AvatarCache;
|
||||||
struct RepositoryView;
|
struct RepositoryView;
|
||||||
|
struct ImFont;
|
||||||
|
|
||||||
class DiffViewer {
|
class DiffViewer {
|
||||||
public:
|
public:
|
||||||
void open(RepositoryView& repository, GitManager& manager, const std::string& path,
|
void open(RepositoryView& repository, GitManager& manager, const std::string& path,
|
||||||
bool staged, std::string& notice);
|
bool staged, std::string& notice);
|
||||||
|
void openCommit(RepositoryView& repository, GitManager& manager, const std::string& path,
|
||||||
|
const std::string& commit_id, std::string& notice);
|
||||||
void close();
|
void close();
|
||||||
bool isOpen() const { return !path_.empty(); }
|
bool isOpen() const { return !path_.empty(); }
|
||||||
void draw(RepositoryView& repository, GitManager& manager, float scale, std::string& notice);
|
void draw(RepositoryView& repository, GitManager& manager, AvatarCache* avatars,
|
||||||
|
float scale, ImFont* code_font, std::string& notice);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class Mode { diff, file, blame, history };
|
enum class Mode { diff, file, blame, history };
|
||||||
@@ -29,17 +34,40 @@ private:
|
|||||||
std::vector<Line> lines;
|
std::vector<Line> lines;
|
||||||
std::string patch;
|
std::string patch;
|
||||||
};
|
};
|
||||||
|
struct BlameLine {
|
||||||
|
std::string hash;
|
||||||
|
std::string author;
|
||||||
|
std::string email;
|
||||||
|
std::string date;
|
||||||
|
std::string summary;
|
||||||
|
std::string text;
|
||||||
|
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 path_;
|
||||||
|
std::string commit_id_;
|
||||||
bool staged_ = false;
|
bool staged_ = false;
|
||||||
Mode mode_ = Mode::diff;
|
Mode mode_ = Mode::diff;
|
||||||
std::string file_header_;
|
std::string file_header_;
|
||||||
std::vector<Hunk> hunks_;
|
std::vector<Hunk> hunks_;
|
||||||
std::vector<std::string> file_lines_;
|
std::vector<std::string> file_lines_;
|
||||||
std::vector<std::string> blame_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;
|
||||||
|
bool minimap_drag_active_ = false;
|
||||||
|
float minimap_drag_offset_ = 0.0f;
|
||||||
|
|
||||||
void reload(RepositoryView& repository, GitManager& manager, std::string& notice);
|
void reload(RepositoryView& repository, GitManager& manager, std::string& notice);
|
||||||
void loadSupplement(RepositoryView& repository, GitManager& manager, Mode mode, std::string& notice);
|
void loadSupplement(RepositoryView& repository, GitManager& manager, Mode mode, std::string& notice);
|
||||||
void parseDiff(const std::string& text);
|
void parseDiff(const std::string& text);
|
||||||
|
void parseBlame(const std::string& text);
|
||||||
|
void parseHistory(const std::string& text);
|
||||||
};
|
};
|
||||||
|
|||||||
3421
src/ui/gitree_ui.cpp
3421
src/ui/gitree_ui.cpp
File diff suppressed because it is too large
Load Diff
@@ -2,10 +2,112 @@
|
|||||||
|
|
||||||
#include "managers/avatar_cache.h"
|
#include "managers/avatar_cache.h"
|
||||||
#include "models/repository.h"
|
#include "models/repository.h"
|
||||||
#include <IconsFontAwesome6.h>
|
#include <IconsTabler.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
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) {
|
||||||
|
static_assert(Count >= 2);
|
||||||
|
draw->PathClear();
|
||||||
|
draw->PathLineTo(points.front());
|
||||||
|
for (size_t index = 1; index + 1 < Count; ++index) {
|
||||||
|
const ImVec2 previous = points[index - 1];
|
||||||
|
const ImVec2 corner = points[index];
|
||||||
|
const ImVec2 next = points[index + 1];
|
||||||
|
const float incoming_length = std::hypot(corner.x - previous.x, corner.y - previous.y);
|
||||||
|
const float outgoing_length = std::hypot(next.x - corner.x, next.y - corner.y);
|
||||||
|
if (incoming_length < 0.01f || outgoing_length < 0.01f) continue;
|
||||||
|
const float turn_radius = std::min({radius, incoming_length * 0.5f, outgoing_length * 0.5f});
|
||||||
|
const ImVec2 incoming{
|
||||||
|
(corner.x - previous.x) / incoming_length,
|
||||||
|
(corner.y - previous.y) / incoming_length,
|
||||||
|
};
|
||||||
|
const ImVec2 outgoing{
|
||||||
|
(next.x - corner.x) / outgoing_length,
|
||||||
|
(next.y - corner.y) / outgoing_length,
|
||||||
|
};
|
||||||
|
const ImVec2 before{
|
||||||
|
corner.x - incoming.x * turn_radius,
|
||||||
|
corner.y - incoming.y * turn_radius,
|
||||||
|
};
|
||||||
|
const ImVec2 after{
|
||||||
|
corner.x + outgoing.x * turn_radius,
|
||||||
|
corner.y + outgoing.y * turn_radius,
|
||||||
|
};
|
||||||
|
constexpr float control = 0.55228475f;
|
||||||
|
draw->PathLineTo(before);
|
||||||
|
draw->PathBezierCubicCurveTo(
|
||||||
|
{before.x + incoming.x * turn_radius * control,
|
||||||
|
before.y + incoming.y * turn_radius * control},
|
||||||
|
{after.x - outgoing.x * turn_radius * control,
|
||||||
|
after.y - outgoing.y * turn_radius * control},
|
||||||
|
after);
|
||||||
|
}
|
||||||
|
draw->PathLineTo(points.back());
|
||||||
|
draw->PathStroke(color, ImDrawFlags_None, thickness);
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawOrthogonalEdge(ImDrawList* draw, const ImVec2& child, const ImVec2& parent,
|
||||||
|
ImU32 color, float scale, int route_slot, float detour_x) {
|
||||||
|
const float thickness = 2.0f * scale;
|
||||||
|
const float horizontal = parent.x - child.x;
|
||||||
|
const float vertical = parent.y - child.y;
|
||||||
|
if (vertical <= 0.0f) {
|
||||||
|
draw->AddLine(child, parent, color, thickness);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offset detour route lines horizontally by slot to avoid lane collisions
|
||||||
|
const float slot_offset = static_cast<float>(route_slot) * 4.0f * scale;
|
||||||
|
|
||||||
|
if (std::isfinite(detour_x)) {
|
||||||
|
const float final_detour_x = detour_x + slot_offset;
|
||||||
|
if (std::abs(final_detour_x - child.x) < 0.5f * scale) {
|
||||||
|
drawRoundedPolyline(draw, std::array{child, ImVec2{final_detour_x, parent.y}, parent},
|
||||||
|
7.0f * scale, color, thickness);
|
||||||
|
} else if (std::abs(final_detour_x - parent.x) < 0.5f * scale) {
|
||||||
|
drawRoundedPolyline(draw, std::array{child, ImVec2{final_detour_x, child.y}, parent},
|
||||||
|
7.0f * scale, color, thickness);
|
||||||
|
} else {
|
||||||
|
drawRoundedPolyline(draw,
|
||||||
|
std::array{child, ImVec2{final_detour_x, child.y}, ImVec2{final_detour_x, parent.y}, parent},
|
||||||
|
7.0f * scale, color, thickness);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std::abs(horizontal) < 0.5f * scale) {
|
||||||
|
draw->AddLine(child, parent, color, thickness);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bends remain anchored to commit rows, but use a compact quarter-round turn.
|
||||||
|
if (route_slot == 0)
|
||||||
|
drawRoundedPolyline(draw, std::array{child, ImVec2{child.x, parent.y}, parent},
|
||||||
|
7.0f * scale, color, thickness);
|
||||||
|
else
|
||||||
|
drawRoundedPolyline(draw, std::array{child, ImVec2{parent.x, child.y}, parent},
|
||||||
|
7.0f * scale, color, thickness);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
ImU32 GraphRenderer::laneColor(int lane, int alpha) {
|
ImU32 GraphRenderer::laneColor(int lane, int alpha) {
|
||||||
static constexpr ImVec4 colors[] = {
|
static constexpr ImVec4 colors[] = {
|
||||||
@@ -25,35 +127,47 @@ ImU32 GraphRenderer::laneColor(int lane, int alpha) {
|
|||||||
float GraphRenderer::requiredWidth(const std::vector<CommitInfo>& commits, float scale) {
|
float GraphRenderer::requiredWidth(const std::vector<CommitInfo>& commits, float scale) {
|
||||||
int maximum_lane = 0;
|
int maximum_lane = 0;
|
||||||
for (const auto& commit : commits) maximum_lane = std::max(maximum_lane, commit.lane);
|
for (const auto& commit : commits) maximum_lane = std::max(maximum_lane, commit.lane);
|
||||||
return std::clamp((42.0f + maximum_lane * 18.0f) * scale, 56.0f * scale, 220.0f * scale);
|
// Keep one spare lane available for edges that must route around occupied commit lanes.
|
||||||
|
return std::max((68.0f + maximum_lane * 28.0f) * scale, 82.0f * scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GraphRenderer::drawRow(int row, const CommitInfo& commit,
|
void GraphRenderer::drawRow(int row, const CommitInfo& commit,
|
||||||
const std::vector<CommitInfo>& commits, const std::vector<float>& row_heights,
|
const std::vector<CommitInfo>& commits, const std::vector<float>& row_heights,
|
||||||
const std::vector<std::vector<int>>& parent_rows, AvatarCache* avatars) const {
|
const std::vector<std::vector<int>>& parent_rows, AvatarCache* avatars,
|
||||||
|
const ImVec2* ref_connector_start) const {
|
||||||
ImDrawList* draw = ImGui::GetWindowDrawList();
|
ImDrawList* draw = ImGui::GetWindowDrawList();
|
||||||
const ImVec2 origin = ImGui::GetCursorScreenPos();
|
const ImVec2 origin = ImGui::GetCursorScreenPos();
|
||||||
const float row_height = row_heights[static_cast<size_t>(row)];
|
const float row_height = row_heights[static_cast<size_t>(row)];
|
||||||
const float content_height = std::max(px(1.0f), row_height - ImGui::GetStyle().CellPadding.y * 2.0f);
|
const float content_height = std::max(px(1.0f), row_height - ImGui::GetStyle().CellPadding.y * 2.0f);
|
||||||
const float lane_spacing = px(18.0f);
|
const float lane_spacing = px(28.0f);
|
||||||
const float x = origin.x + px(15.0f) + lane_spacing * commit.lane;
|
const float x = origin.x + px(17.0f) + lane_spacing * commit.lane;
|
||||||
const float y = origin.y + content_height * 0.5f;
|
const float y = origin.y + content_height * 0.5f;
|
||||||
const float cell_right = origin.x + ImGui::GetContentRegionAvail().x;
|
const float cell_right = origin.x + ImGui::GetContentRegionAvail().x;
|
||||||
const float row_clip_padding = ImGui::GetStyle().CellPadding.y + px(1.0f);
|
const float row_clip_padding = ImGui::GetStyle().CellPadding.y + px(1.0f);
|
||||||
|
|
||||||
// GitKraken-style lane ribbon: a quiet tint carries the branch color through
|
// Start the lane ribbon at the profile circle's centerline. The opaque node backing
|
||||||
// the rest of the graph column while the far edge remains crisply identifiable.
|
// masks its left edge, while the vertical inset keeps neighboring row tints separate.
|
||||||
|
const float ribbon_left = std::min(x, cell_right);
|
||||||
|
const float ribbon_top = origin.y + px(1.0f);
|
||||||
|
const float ribbon_bottom = origin.y + content_height - px(1.0f);
|
||||||
draw->AddRectFilled(
|
draw->AddRectFilled(
|
||||||
{x, origin.y - row_clip_padding},
|
{ribbon_left, ribbon_top},
|
||||||
{cell_right, origin.y + content_height + row_clip_padding},
|
{cell_right, ribbon_bottom},
|
||||||
laneColor(commit.lane, 38));
|
laneColor(commit.graph_color, 38));
|
||||||
draw->AddLine(
|
draw->AddLine(
|
||||||
{cell_right - px(1.0f), origin.y - row_clip_padding},
|
{cell_right - px(1.0f), ribbon_top},
|
||||||
{cell_right - px(1.0f), origin.y + content_height + row_clip_padding},
|
{cell_right - px(1.0f), ribbon_bottom},
|
||||||
laneColor(commit.lane, 220), px(2.0f));
|
laneColor(commit.graph_color, 215), px(2.0f));
|
||||||
if (!commit.refs.empty())
|
if (ref_connector_start) {
|
||||||
draw->AddLine({origin.x - ImGui::GetStyle().CellPadding.x, y}, {x, y},
|
const ImVec2 end{x, y};
|
||||||
laneColor(commit.lane, 150), px(1.0f));
|
const float turn_x = end.x - px(8.0f);
|
||||||
|
draw->PushClipRectFullScreen();
|
||||||
|
drawRoundedPolyline(draw,
|
||||||
|
std::array{*ref_connector_start, ImVec2{turn_x, ref_connector_start->y},
|
||||||
|
ImVec2{turn_x, end.y}, end},
|
||||||
|
px(5.0f), laneColor(commit.graph_color, 190), px(1.8f));
|
||||||
|
draw->PopClipRect();
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<float> row_offsets(row_heights.size() + 1, 0.0f);
|
std::vector<float> row_offsets(row_heights.size() + 1, 0.0f);
|
||||||
for (size_t index = 0; index < row_heights.size(); ++index)
|
for (size_t index = 0; index < row_heights.size(); ++index)
|
||||||
@@ -64,57 +178,124 @@ void GraphRenderer::drawRow(int row, const CommitInfo& commit,
|
|||||||
(row_heights[static_cast<size_t>(index)] - ImGui::GetStyle().CellPadding.y * 2.0f) * 0.5f;
|
(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
|
// Every row redraws edges crossing its clip rectangle. This keeps long paths continuous
|
||||||
// without allowing table row clipping to cut out intermediate segments.
|
// without allowing table row clipping to cut out intermediate segments.
|
||||||
draw->PushClipRect(
|
draw->PushClipRect(
|
||||||
{origin.x, origin.y - row_clip_padding},
|
{origin.x, origin.y - row_clip_padding},
|
||||||
{origin.x + ImGui::GetContentRegionAvail().x, origin.y + content_height + row_clip_padding}, true);
|
{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) {
|
for (const RoutedEdge& route : routed_edges) {
|
||||||
if (row_heights[static_cast<size_t>(child_row)] <= 0.0f) continue;
|
if (row < route.child_row || row > route.parent_row ||
|
||||||
const CommitInfo& child = commits[static_cast<size_t>(child_row)];
|
row_heights[static_cast<size_t>(route.child_row)] <= 0.0f ||
|
||||||
for (const int parent_row : parent_rows[static_cast<size_t>(child_row)]) {
|
row_heights[static_cast<size_t>(route.parent_row)] <= 0.0f)
|
||||||
if (parent_row <= child_row || row < child_row || row > parent_row ||
|
continue;
|
||||||
row_heights[static_cast<size_t>(parent_row)] <= 0.0f) continue;
|
|
||||||
const CommitInfo& parent = commits[static_cast<size_t>(parent_row)];
|
|
||||||
|
|
||||||
const float child_x = origin.x + px(15.0f) + lane_spacing * child.lane;
|
const float child_x = origin.x + px(17.0f) + lane_spacing * route.child_lane;
|
||||||
const float parent_x = origin.x + px(15.0f) + lane_spacing * parent.lane;
|
const float parent_x = origin.x + px(17.0f) + lane_spacing * route.parent_lane;
|
||||||
const float child_y = center_y(child_row);
|
const float child_y = center_y(route.child_row);
|
||||||
const float parent_y = center_y(parent_row);
|
const float parent_y = center_y(route.parent_row);
|
||||||
// An edge belongs to the branch it leaves, including the curved transition
|
const float detour_x = route.detour_lane >= 0
|
||||||
// into a parent lane, so its color stays consistent with the child node/ref chip.
|
? origin.x + px(17.0f) + lane_spacing * route.detour_lane
|
||||||
const ImU32 color = laneColor(child.lane, 225);
|
: std::numeric_limits<float>::quiet_NaN();
|
||||||
if (child.lane == parent.lane) {
|
|
||||||
draw->AddLine({child_x, child_y}, {parent_x, parent_y}, color, px(1.8f));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const float bend_height = std::min(parent_y - child_y, px(24.0f));
|
drawOrthogonalEdge(draw, {child_x, child_y}, {parent_x, parent_y},
|
||||||
const float curve_end_y = child_y + bend_height;
|
laneColor(route.color_lane, 235), scale_, route.route_slot, detour_x);
|
||||||
draw->AddBezierCubic(
|
|
||||||
{child_x, child_y},
|
|
||||||
{child_x, child_y + bend_height * 0.45f},
|
|
||||||
{parent_x, child_y + bend_height * 0.55f},
|
|
||||||
{parent_x, curve_end_y},
|
|
||||||
color, px(1.8f));
|
|
||||||
if (curve_end_y < parent_y)
|
|
||||||
draw->AddLine({parent_x, curve_end_y}, {parent_x, parent_y}, color, px(1.8f));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
draw->PopClipRect();
|
draw->PopClipRect();
|
||||||
|
|
||||||
const ImU32 lane_color = laneColor(commit.lane);
|
const ImU32 lane_color = laneColor(commit.graph_color);
|
||||||
|
const auto show_identity_tooltip = [&](float hit_radius) {
|
||||||
|
if (!ImGui::IsWindowHovered()) return;
|
||||||
|
const ImVec2 mouse = ImGui::GetIO().MousePos;
|
||||||
|
const float delta_x = mouse.x - x;
|
||||||
|
const float delta_y = mouse.y - y;
|
||||||
|
if (delta_x * delta_x + delta_y * delta_y > hit_radius * hit_radius) return;
|
||||||
|
if (commit.email.empty()) ImGui::SetTooltip("%s", commit.author.c_str());
|
||||||
|
else ImGui::SetTooltip("%s <%s>", commit.author.c_str(), commit.email.c_str());
|
||||||
|
};
|
||||||
if (commit.parents > 1) {
|
if (commit.parents > 1) {
|
||||||
draw->AddCircleFilled({x, y}, px(4.5f), lane_color);
|
draw->AddCircleFilled({x, y}, px(6.0f), lane_color);
|
||||||
draw->AddCircle({x, y}, px(6.0f), laneColor(commit.lane, 110), 0, px(1.0f));
|
draw->AddCircle({x, y}, px(7.5f), laneColor(commit.graph_color, 130), 0, px(1.2f));
|
||||||
|
show_identity_tooltip(px(9.0f));
|
||||||
ImGui::Dummy({0.0f, content_height});
|
ImGui::Dummy({0.0f, content_height});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const float radius = px(8.4f);
|
const float radius = px(9.6f);
|
||||||
// The opaque backing masks every lane segment before the avatar is painted.
|
// The opaque backing masks every lane segment before the avatar is painted.
|
||||||
draw->AddCircleFilled({x, y}, radius + px(2.2f), IM_COL32(19, 24, 31, 255));
|
draw->AddCircleFilled({x, y}, radius + px(1.8f), IM_COL32(19, 24, 31, 255));
|
||||||
draw->AddCircle({x, y}, radius + px(1.0f), lane_color, 0, px(2.0f));
|
draw->AddCircle({x, y}, radius + px(0.8f), lane_color, 0, px(2.0f));
|
||||||
draw->AddCircleFilled({x, y}, radius - px(1.0f), IM_COL32(232, 238, 242, 255));
|
draw->AddCircleFilled({x, y}, radius - px(1.0f), IM_COL32(232, 238, 242, 255));
|
||||||
|
|
||||||
const unsigned int texture = avatars ? avatars->textureFor(commit.email) : 0;
|
const unsigned int texture = avatars ? avatars->textureFor(commit.email) : 0;
|
||||||
@@ -124,8 +305,9 @@ void GraphRenderer::drawRow(int row, const CommitInfo& commit,
|
|||||||
{x + radius - px(1.5f), y + radius - px(1.5f)},
|
{x + radius - px(1.5f), y + radius - px(1.5f)},
|
||||||
{0, 0}, {1, 1}, IM_COL32_WHITE, radius);
|
{0, 0}, {1, 1}, IM_COL32_WHITE, radius);
|
||||||
} else {
|
} else {
|
||||||
const ImVec2 icon_size = ImGui::CalcTextSize(ICON_FA_USER);
|
const ImVec2 icon_size = ImGui::CalcTextSize(ICON_TB_USER);
|
||||||
draw->AddText({x - icon_size.x * 0.5f, y - icon_size.y * 0.5f}, lane_color, ICON_FA_USER);
|
draw->AddText({x - icon_size.x * 0.5f, y - icon_size.y * 0.5f}, lane_color, ICON_TB_USER);
|
||||||
}
|
}
|
||||||
|
show_identity_tooltip(radius + px(2.0f));
|
||||||
ImGui::Dummy({0.0f, content_height});
|
ImGui::Dummy({0.0f, content_height});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public:
|
|||||||
|
|
||||||
void drawRow(int row, const CommitInfo& commit, const std::vector<CommitInfo>& commits,
|
void drawRow(int row, const CommitInfo& commit, const std::vector<CommitInfo>& commits,
|
||||||
const std::vector<float>& row_heights, const std::vector<std::vector<int>>& parent_rows,
|
const std::vector<float>& row_heights, const std::vector<std::vector<int>>& parent_rows,
|
||||||
AvatarCache* avatars) const;
|
AvatarCache* avatars, const ImVec2* ref_connector_start = nullptr) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
float px(float value) const { return value * scale_; }
|
float px(float value) const { return value * scale_; }
|
||||||
|
|||||||
BIN
vendor/fonts/Inter-Regular.ttf
vendored
Normal file
BIN
vendor/fonts/Inter-Regular.ttf
vendored
Normal file
Binary file not shown.
BIN
vendor/fonts/Inter-SemiBold.ttf
vendored
Normal file
BIN
vendor/fonts/Inter-SemiBold.ttf
vendored
Normal file
Binary file not shown.
BIN
vendor/fonts/tabler-icons-outline.ttf
vendored
BIN
vendor/fonts/tabler-icons-outline.ttf
vendored
Binary file not shown.
2
vendor/glfw
vendored
2
vendor/glfw
vendored
Submodule vendor/glfw updated: 7b6aead9fb...567b1ec244
1
vendor/iKv
vendored
1
vendor/iKv
vendored
Submodule vendor/iKv deleted from d0b02f4735
1
vendor/iKvxx
vendored
Submodule
1
vendor/iKvxx
vendored
Submodule
Submodule vendor/iKvxx added at 99d3ea7b83
2
vendor/iZo
vendored
2
vendor/iZo
vendored
Submodule vendor/iZo updated: 80c6bfce90...355a757a17
76
vendor/icons/IconsTabler.h
vendored
Normal file
76
vendor/icons/IconsTabler.h
vendored
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Tabler Icons 3.44.0 outline webfont glyphs used by Gitree.
|
||||||
|
#define ICON_MIN_TB 0xEA06
|
||||||
|
#define ICON_MAX_TB 0xFAF7
|
||||||
|
|
||||||
|
#define ICON_TB_ANGLE_RIGHT "\xee\xa9\xa1"
|
||||||
|
#define ICON_TB_ARROW_DOWN "\xee\xa8\x96"
|
||||||
|
#define ICON_TB_ARROW_DOWN_A_Z "\xee\xbc\x98"
|
||||||
|
#define ICON_TB_ARROW_DOWN_Z_A "\xee\xbc\x9a"
|
||||||
|
#define ICON_TB_ARROW_RIGHT_ARROW_LEFT "\xef\x87\xb4"
|
||||||
|
#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"
|
||||||
|
#define ICON_TB_CHECK "\xee\xa9\x9e"
|
||||||
|
#define ICON_TB_CHEVRON_DOWN "\xee\xa9\x9f"
|
||||||
|
#define ICON_TB_CHEVRON_RIGHT "\xee\xa9\xa1"
|
||||||
|
#define ICON_TB_CIRCLE_CHEVRON_LEFT "\xef\x98\xa3"
|
||||||
|
#define ICON_TB_CIRCLE_CHEVRON_RIGHT "\xef\x98\xa4"
|
||||||
|
#define ICON_TB_CIRCLE_DOT "\xee\xbe\xb1"
|
||||||
|
#define ICON_TB_CIRCLE_NODES "\xee\xaa\xb5"
|
||||||
|
#define ICON_TB_CLOCK_ROTATE_LEFT "\xee\xaf\xaa"
|
||||||
|
#define ICON_TB_CLOUD "\xee\xa9\xb6"
|
||||||
|
#define ICON_TB_CODE "\xee\xa9\xb7"
|
||||||
|
#define ICON_TB_CODE_BRANCH "\xee\xaa\xb2"
|
||||||
|
#define ICON_TB_COMPUTER "\xee\xaa\x89"
|
||||||
|
#define ICON_TB_COPY "\xee\xa9\xba"
|
||||||
|
#define ICON_TB_CUBES "\xee\xb8\x97"
|
||||||
|
#define ICON_TB_DESKTOP "\xee\xaa\x89"
|
||||||
|
#define ICON_TB_DEVICE_LAPTOP "\xee\xad\xa4"
|
||||||
|
#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"
|
||||||
|
#define ICON_TB_ROBOT "\xef\x80\x8b"
|
||||||
|
#define ICON_TB_ROTATE_LEFT "\xee\xac\x96"
|
||||||
|
#define ICON_TB_ROTATE_RIGHT "\xee\xac\x95"
|
||||||
|
#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"
|
||||||
|
#define ICON_TB_UPLOAD "\xee\xad\x87"
|
||||||
|
#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/imgui
vendored
2
vendor/imgui
vendored
Submodule vendor/imgui updated: b61e56346a...d15966ff6c
2
vendor/libgit2
vendored
2
vendor/libgit2
vendored
Submodule vendor/libgit2 updated: f7164261c9...44c05e5d12
Reference in New Issue
Block a user