Compare commits
9 Commits
prod-12-2e
...
release-9a
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a274c148e | |||
| b6400880e8 | |||
| fb98fe6106 | |||
| c99f0cc34a | |||
| 5c6cd4649f | |||
| 0b220a382e | |||
| 5a2cffc177 | |||
| 6214c97b28 | |||
| 2c7cabb0f9 |
@@ -1,12 +1,16 @@
|
||||
name: Build Windows EXE
|
||||
name: Build Releases
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
|
||||
env:
|
||||
APP_NAME: gitree
|
||||
|
||||
jobs:
|
||||
build-windows:
|
||||
name: Build Windows x64
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -16,6 +20,16 @@ jobs:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate build names
|
||||
env:
|
||||
GITEA_SHA: ${{ gitea.sha }}
|
||||
run: |
|
||||
short_sha="$(printf '%s' "$GITEA_SHA" | cut -c1-7)"
|
||||
|
||||
echo "BUILD_HASH=${short_sha}" >> "$GITHUB_ENV"
|
||||
echo "WINDOWS_EXE=${APP_NAME}-windows-x64-${short_sha}.exe" >> "$GITHUB_ENV"
|
||||
echo "WINDOWS_ARTIFACT=${APP_NAME}-windows-x64-${short_sha}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -26,10 +40,13 @@ jobs:
|
||||
cat > mingw-toolchain.cmake <<'EOF'
|
||||
set(CMAKE_SYSTEM_NAME Windows)
|
||||
set(CMAKE_SYSTEM_PROCESSOR x86_64)
|
||||
|
||||
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
|
||||
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
|
||||
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
|
||||
|
||||
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
|
||||
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
@@ -46,9 +63,32 @@ jobs:
|
||||
- name: Build Windows executable
|
||||
run: cmake --build build-win --parallel
|
||||
|
||||
- name: Locate Windows EXE
|
||||
run: |
|
||||
exe_path=""
|
||||
|
||||
if [ -f "build-win/bin/${APP_NAME}.exe" ]; then
|
||||
exe_path="build-win/bin/${APP_NAME}.exe"
|
||||
elif [ -f "build-win/bin/Gitree.exe" ]; then
|
||||
exe_path="build-win/bin/Gitree.exe"
|
||||
elif [ -f "build-win/bin/gitree.exe" ]; then
|
||||
exe_path="build-win/bin/gitree.exe"
|
||||
else
|
||||
exe_path="$(find build-win -type f -iname '*.exe' | head -n 1)"
|
||||
fi
|
||||
|
||||
if [ -z "$exe_path" ]; then
|
||||
echo "Could not find built Windows EXE."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "WINDOWS_EXE_PATH=${exe_path}" >> "$GITHUB_ENV"
|
||||
echo "Found Windows EXE: ${exe_path}"
|
||||
|
||||
- name: Verify static runtime linkage
|
||||
run: |
|
||||
dependencies="$(x86_64-w64-mingw32-objdump -p build-win/bin/gitree.exe | sed -n 's/.*DLL Name: //p')"
|
||||
dependencies="$(x86_64-w64-mingw32-objdump -p "$WINDOWS_EXE_PATH" | sed -n 's/.*DLL Name: //p')"
|
||||
|
||||
printf '%s\n' "$dependencies"
|
||||
|
||||
if printf '%s\n' "$dependencies" | grep -Eqi 'lib(gcc|stdc\+\+|winpthread).*\.dll'; then
|
||||
@@ -56,36 +96,174 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Generate EXE name
|
||||
run: |
|
||||
repo_name="$(basename "${GITEA_REPOSITORY}")"
|
||||
short_sha="$(printf '%s' "${GITEA_SHA}" | cut -c1-7)"
|
||||
|
||||
exe_name="${repo_name}-windows-x64-${GITEA_RUN_NUMBER}-${short_sha}.exe"
|
||||
|
||||
echo "EXE_NAME=${exe_name}" >> "$GITHUB_ENV"
|
||||
echo "ARTIFACT_NAME=${repo_name}-windows-x64-${GITEA_RUN_NUMBER}-${short_sha}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Prepare artifact
|
||||
- name: Prepare Windows artifact
|
||||
run: |
|
||||
mkdir -p dist
|
||||
cp build-win/bin/gitree.exe "dist/${EXE_NAME}"
|
||||
cp "$WINDOWS_EXE_PATH" "dist/${WINDOWS_EXE}"
|
||||
|
||||
- name: Upload Windows build
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
path: dist/${{ env.EXE_NAME }}
|
||||
name: ${{ env.WINDOWS_ARTIFACT }}
|
||||
path: dist/${{ env.WINDOWS_EXE }}
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Create prod release
|
||||
if: ${{ gitea.ref_name == 'prod' }}
|
||||
build-linux:
|
||||
name: Build Linux x64 DEB
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate build names
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
GITEA_SHA: ${{ gitea.sha }}
|
||||
GITEA_RUN_NUMBER: ${{ gitea.run_number }}
|
||||
run: |
|
||||
short_sha="$(printf '%s' "$GITEA_SHA" | cut -c1-7)"
|
||||
|
||||
echo "BUILD_HASH=${short_sha}" >> "$GITHUB_ENV"
|
||||
echo "LINUX_DEB=${APP_NAME}-deb-x64-${short_sha}.deb" >> "$GITHUB_ENV"
|
||||
echo "LINUX_ARTIFACT=${APP_NAME}-deb-x64-${short_sha}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y cmake ninja-build curl jq dpkg-dev
|
||||
|
||||
- name: Configure Linux release build
|
||||
run: |
|
||||
cmake -S . -B build-linux -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_SHARED_LIBS=OFF
|
||||
|
||||
- name: Build Linux executable
|
||||
run: cmake --build build-linux --parallel
|
||||
|
||||
- name: Locate Linux executable
|
||||
run: |
|
||||
exe_path=""
|
||||
|
||||
if [ -f "build-linux/bin/${APP_NAME}" ]; then
|
||||
exe_path="build-linux/bin/${APP_NAME}"
|
||||
elif [ -f "build-linux/bin/Gitree" ]; then
|
||||
exe_path="build-linux/bin/Gitree"
|
||||
elif [ -f "build-linux/bin/gitree" ]; then
|
||||
exe_path="build-linux/bin/gitree"
|
||||
else
|
||||
exe_path="$(find build-linux -type f -executable -name "${APP_NAME}" | head -n 1)"
|
||||
fi
|
||||
|
||||
if [ -z "$exe_path" ]; then
|
||||
echo "Could not find built Linux executable."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "LINUX_EXE_PATH=${exe_path}" >> "$GITHUB_ENV"
|
||||
echo "Found Linux executable: ${exe_path}"
|
||||
|
||||
- name: Package Linux DEB
|
||||
run: |
|
||||
mkdir -p "pkg/DEBIAN"
|
||||
mkdir -p "pkg/usr/bin"
|
||||
|
||||
cp "$LINUX_EXE_PATH" "pkg/usr/bin/${APP_NAME}"
|
||||
chmod 755 "pkg/usr/bin/${APP_NAME}"
|
||||
|
||||
version="0.0.0+${BUILD_HASH}"
|
||||
|
||||
cat > "pkg/DEBIAN/control" <<EOF
|
||||
Package: ${APP_NAME}
|
||||
Version: ${version}
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Architecture: amd64
|
||||
Maintainer: GigabiteStudios <ceo@gigabitestudios.xyz>
|
||||
Description: ${APP_NAME}
|
||||
${APP_NAME} Linux x64 build.
|
||||
EOF
|
||||
|
||||
mkdir -p dist
|
||||
dpkg-deb --build pkg "dist/${LINUX_DEB}"
|
||||
|
||||
- name: Upload Linux DEB build
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ env.LINUX_ARTIFACT }}
|
||||
path: dist/${{ env.LINUX_DEB }}
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-windows
|
||||
- build-linux
|
||||
if: ${{ always() && gitea.ref_name == 'prod' }}
|
||||
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
GITEA_SHA: ${{ gitea.sha }}
|
||||
WINDOWS_RESULT: ${{ needs.build-windows.result }}
|
||||
LINUX_RESULT: ${{ needs.build-linux.result }}
|
||||
APP_NAME: gitree
|
||||
|
||||
steps:
|
||||
- name: Check build results
|
||||
run: |
|
||||
echo "Windows result: ${WINDOWS_RESULT}"
|
||||
echo "Linux result: ${LINUX_RESULT}"
|
||||
|
||||
if [ "$WINDOWS_RESULT" != "success" ] && [ "$LINUX_RESULT" != "success" ]; then
|
||||
echo "Both Windows and Linux builds failed. Refusing to create release."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate release names
|
||||
run: |
|
||||
short_sha="$(printf '%s' "$GITEA_SHA" | cut -c1-7)"
|
||||
|
||||
echo "BUILD_HASH=${short_sha}" >> "$GITHUB_ENV"
|
||||
echo "RELEASE_TAG=release-${short_sha}" >> "$GITHUB_ENV"
|
||||
echo "RELEASE_NAME=Release ${short_sha}" >> "$GITHUB_ENV"
|
||||
|
||||
echo "WINDOWS_EXE=${APP_NAME}-windows-x64-${short_sha}.exe" >> "$GITHUB_ENV"
|
||||
echo "WINDOWS_ARTIFACT=${APP_NAME}-windows-x64-${short_sha}" >> "$GITHUB_ENV"
|
||||
|
||||
echo "LINUX_DEB=${APP_NAME}-deb-x64-${short_sha}.deb" >> "$GITHUB_ENV"
|
||||
echo "LINUX_ARTIFACT=${APP_NAME}-deb-x64-${short_sha}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install release dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y curl jq
|
||||
|
||||
- name: Download Windows artifact
|
||||
if: ${{ needs.build-windows.result == 'success' }}
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ env.WINDOWS_ARTIFACT }}
|
||||
path: release-assets
|
||||
|
||||
- name: Download Linux artifact
|
||||
if: ${{ needs.build-linux.result == 'success' }}
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ env.LINUX_ARTIFACT }}
|
||||
path: release-assets
|
||||
|
||||
- name: Create prod release
|
||||
run: |
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
echo "The repository secret GITEA_TOKEN is required to publish prod releases."
|
||||
@@ -94,8 +272,6 @@ jobs:
|
||||
|
||||
git fetch --tags --force
|
||||
|
||||
short_sha="$(printf '%s' "$GITEA_SHA" | cut -c1-7)"
|
||||
tag="prod-${GITEA_RUN_NUMBER}-${short_sha}"
|
||||
api="${GITEA_SERVER_URL%/}/api/v1/repos/${GITEA_REPOSITORY}"
|
||||
repo_url="${GITEA_SERVER_URL%/}/${GITEA_REPOSITORY}"
|
||||
|
||||
@@ -103,21 +279,16 @@ jobs:
|
||||
curl --fail-with-body --silent --show-error \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${api}/releases?limit=50" |
|
||||
jq -r '[.[] | select(.tag_name | startswith("prod-"))][0].tag_name // empty'
|
||||
jq -r '[.[] | select(.tag_name | startswith("release-"))][0].tag_name // empty'
|
||||
)"
|
||||
|
||||
if [ -n "$previous_tag" ]; then
|
||||
range="${previous_tag}..${GITEA_SHA}"
|
||||
echo "Generating release notes from ${previous_tag} to ${GITEA_SHA}"
|
||||
else
|
||||
range="${GITEA_SHA}"
|
||||
echo "No previous prod release found. Generating release notes from current commit."
|
||||
fi
|
||||
|
||||
{
|
||||
echo "Automated Windows release from the prod branch."
|
||||
echo
|
||||
|
||||
if [ -n "$previous_tag" ]; then
|
||||
echo "## Changes since ${previous_tag}"
|
||||
else
|
||||
@@ -140,6 +311,22 @@ jobs:
|
||||
done
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "## Builds"
|
||||
echo
|
||||
|
||||
if [ "$WINDOWS_RESULT" = "success" ]; then
|
||||
echo "- ${WINDOWS_EXE}"
|
||||
else
|
||||
echo "- Windows x64 failed"
|
||||
fi
|
||||
|
||||
if [ "$LINUX_RESULT" = "success" ]; then
|
||||
echo "- ${LINUX_DEB}"
|
||||
else
|
||||
echo "- Linux DEB x64 failed"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "## Authors"
|
||||
echo
|
||||
@@ -175,9 +362,9 @@ jobs:
|
||||
} > release-notes.md
|
||||
|
||||
payload="$(jq -n \
|
||||
--arg tag "$tag" \
|
||||
--arg tag "$RELEASE_TAG" \
|
||||
--arg sha "$GITEA_SHA" \
|
||||
--arg name "${ARTIFACT_NAME}" \
|
||||
--arg name "$RELEASE_NAME" \
|
||||
--rawfile body release-notes.md \
|
||||
'{
|
||||
tag_name: $tag,
|
||||
@@ -197,8 +384,18 @@ jobs:
|
||||
|
||||
release_id="$(printf '%s' "$release" | jq -er '.id')"
|
||||
|
||||
curl --fail-with-body --silent --show-error \
|
||||
-X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-F "attachment=@dist/${EXE_NAME}" \
|
||||
"${api}/releases/${release_id}/assets?name=${EXE_NAME}"
|
||||
for asset in release-assets/*; do
|
||||
if [ ! -f "$asset" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
asset_name="$(basename "$asset")"
|
||||
|
||||
echo "Uploading ${asset_name}"
|
||||
|
||||
curl --fail-with-body --silent --show-error \
|
||||
-X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-F "attachment=@${asset}" \
|
||||
"${api}/releases/${release_id}/assets?name=${asset_name}"
|
||||
done
|
||||
@@ -14,20 +14,31 @@ namespace
|
||||
return value && !value->empty() ? std::filesystem::path(*value) : std::filesystem::path{};
|
||||
}
|
||||
|
||||
std::filesystem::path under(const std::filesystem::path &root, const char *relative)
|
||||
{
|
||||
return root.empty() ? std::filesystem::path{} : root / relative;
|
||||
}
|
||||
|
||||
std::filesystem::path firstExisting(std::initializer_list<std::filesystem::path> candidates)
|
||||
{
|
||||
std::error_code error;
|
||||
|
||||
for (const auto &candidate : candidates)
|
||||
{
|
||||
if (!candidate.empty() && std::filesystem::is_regular_file(candidate, error))
|
||||
return candidate;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::filesystem::path findExecutable(const std::filesystem::path &root, const char *filename)
|
||||
{
|
||||
std::error_code error;
|
||||
|
||||
if (root.empty() || !std::filesystem::is_directory(root, error))
|
||||
return {};
|
||||
|
||||
for (std::filesystem::recursive_directory_iterator iterator(
|
||||
root, std::filesystem::directory_options::skip_permission_denied, error),
|
||||
end;
|
||||
@@ -36,6 +47,7 @@ namespace
|
||||
if (iterator->is_regular_file(error) && iterator->path().filename() == filename)
|
||||
return iterator->path();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -43,9 +55,14 @@ namespace
|
||||
ApplicationManager::ApplicationManager()
|
||||
{
|
||||
const auto local = izo::GetKnownPath(izo::KnownPath::LocalAppData);
|
||||
const auto roaming = izo::GetKnownPath(izo::KnownPath::AppData);
|
||||
|
||||
const auto program_files = environmentPath("ProgramFiles");
|
||||
const auto program_files_x86 = environmentPath("ProgramFiles(x86)");
|
||||
const auto program_data = environmentPath("ProgramData");
|
||||
const auto user_profile = environmentPath("USERPROFILE");
|
||||
const auto windows = environmentPath("WINDIR");
|
||||
const auto chocolatey = environmentPath("ChocolateyInstall");
|
||||
|
||||
auto add = [this](ExternalApplicationId id, const char *name, std::filesystem::path executable,
|
||||
std::vector<std::string> arguments = {})
|
||||
@@ -55,99 +72,481 @@ ApplicationManager::ApplicationManager()
|
||||
applications_.push_back(targets_.back().info);
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// VS Code-style editors
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
add(ExternalApplicationId::visual_studio_code, "VS Code", firstExisting({
|
||||
local / "Programs/Microsoft VS Code/Code.exe",
|
||||
program_files / "Microsoft VS Code/Code.exe",
|
||||
program_files_x86 / "Microsoft VS Code/Code.exe",
|
||||
}));
|
||||
add(ExternalApplicationId::visual_studio, "Visual Studio", firstExisting({
|
||||
program_files / "Microsoft Visual Studio/2022/Community/Common7/IDE/devenv.exe",
|
||||
program_files / "Microsoft Visual Studio/2022/Professional/Common7/IDE/devenv.exe",
|
||||
program_files / "Microsoft Visual Studio/2022/Enterprise/Common7/IDE/devenv.exe",
|
||||
program_files_x86 / "Microsoft Visual Studio/2019/Community/Common7/IDE/devenv.exe",
|
||||
}));
|
||||
under(local, "Programs/Microsoft VS Code/Code.exe"),
|
||||
under(program_files, "Microsoft VS Code/Code.exe"),
|
||||
under(program_files_x86, "Microsoft VS Code/Code.exe"),
|
||||
under(user_profile, "scoop/apps/vscode/current/Code.exe"),
|
||||
under(chocolatey, "lib/vscode/tools/Code.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::visual_studio_code_insiders, "VS Code Insiders", firstExisting({
|
||||
under(local, "Programs/Microsoft VS Code Insiders/Code - Insiders.exe"),
|
||||
under(program_files, "Microsoft VS Code Insiders/Code - Insiders.exe"),
|
||||
under(user_profile, "scoop/apps/vscode-insiders/current/Code - Insiders.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::vscodium, "VSCodium", firstExisting({
|
||||
under(local, "Programs/VSCodium/VSCodium.exe"),
|
||||
under(program_files, "VSCodium/VSCodium.exe"),
|
||||
under(program_files_x86, "VSCodium/VSCodium.exe"),
|
||||
under(user_profile, "scoop/apps/vscodium/current/VSCodium.exe"),
|
||||
under(chocolatey, "lib/vscodium/tools/VSCodium.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::cursor, "Cursor", firstExisting({
|
||||
under(local, "Programs/cursor/Cursor.exe"),
|
||||
under(local, "Programs/Cursor/Cursor.exe"),
|
||||
under(program_files, "Cursor/Cursor.exe"),
|
||||
under(user_profile, "scoop/apps/cursor/current/Cursor.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::windsurf, "Windsurf", firstExisting({
|
||||
under(local, "Programs/Windsurf/Windsurf.exe"),
|
||||
under(program_files, "Windsurf/Windsurf.exe"),
|
||||
under(user_profile, "scoop/apps/windsurf/current/Windsurf.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::trae, "Trae", firstExisting({
|
||||
under(local, "Programs/Trae/Trae.exe"),
|
||||
under(program_files, "Trae/Trae.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::zed, "Zed", firstExisting({
|
||||
under(local, "Programs/Zed/Zed.exe"),
|
||||
under(program_files, "Zed/Zed.exe"),
|
||||
under(user_profile, "scoop/apps/zed/current/Zed.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::antigravity, "Antigravity", firstExisting({
|
||||
local / "Programs/Antigravity/Antigravity.exe",
|
||||
local / "Antigravity/Antigravity.exe",
|
||||
program_files / "Antigravity/Antigravity.exe",
|
||||
}));
|
||||
under(local, "Programs/Antigravity/Antigravity.exe"),
|
||||
under(local, "Antigravity/Antigravity.exe"),
|
||||
under(program_files, "Antigravity/Antigravity.exe"),
|
||||
}));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Microsoft IDEs
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
add(ExternalApplicationId::visual_studio, "Visual Studio", firstExisting({
|
||||
under(program_files, "Microsoft Visual Studio/2022/Community/Common7/IDE/devenv.exe"),
|
||||
under(program_files, "Microsoft Visual Studio/2022/Professional/Common7/IDE/devenv.exe"),
|
||||
under(program_files, "Microsoft Visual Studio/2022/Enterprise/Common7/IDE/devenv.exe"),
|
||||
under(program_files, "Microsoft Visual Studio/2022/Preview/Common7/IDE/devenv.exe"),
|
||||
|
||||
under(program_files_x86, "Microsoft Visual Studio/2019/Community/Common7/IDE/devenv.exe"),
|
||||
under(program_files_x86, "Microsoft Visual Studio/2019/Professional/Common7/IDE/devenv.exe"),
|
||||
under(program_files_x86, "Microsoft Visual Studio/2019/Enterprise/Common7/IDE/devenv.exe"),
|
||||
under(program_files_x86, "Microsoft Visual Studio/2019/Preview/Common7/IDE/devenv.exe"),
|
||||
}));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// GitHub / Windows tools
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
std::filesystem::path github_desktop = firstExisting({
|
||||
local / "GitHubDesktop/GitHubDesktop.exe",
|
||||
local / "GitHub Desktop/GitHubDesktop.exe",
|
||||
under(local, "GitHubDesktop/GitHubDesktop.exe"),
|
||||
under(local, "GitHub Desktop/GitHubDesktop.exe"),
|
||||
});
|
||||
|
||||
if (github_desktop.empty())
|
||||
github_desktop = findExecutable(local / "GitHubDesktop", "GitHubDesktop.exe");
|
||||
github_desktop = findExecutable(under(local, "GitHubDesktop"), "GitHubDesktop.exe");
|
||||
|
||||
add(ExternalApplicationId::github_desktop, "GitHub Desktop", std::move(github_desktop));
|
||||
add(ExternalApplicationId::file_explorer, "File Explorer",
|
||||
firstExisting({windows / "explorer.exe"}));
|
||||
add(ExternalApplicationId::terminal, "Terminal",
|
||||
firstExisting({local / "Microsoft/WindowsApps/wt.exe", windows / "System32/wt.exe"}), {"-d"});
|
||||
|
||||
add(ExternalApplicationId::file_explorer, "File Explorer", firstExisting({
|
||||
under(windows, "explorer.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::terminal, "Terminal", firstExisting({
|
||||
under(local, "Microsoft/WindowsApps/wt.exe"),
|
||||
under(windows, "System32/wt.exe"),
|
||||
}), {"-d"});
|
||||
|
||||
add(ExternalApplicationId::git_bash, "Git Bash", firstExisting({
|
||||
program_files / "Git/git-bash.exe",
|
||||
program_files_x86 / "Git/git-bash.exe",
|
||||
}),
|
||||
{"--cd="});
|
||||
add(ExternalApplicationId::wsl, "WSL", firstExisting({windows / "System32/wsl.exe"}), {"--cd"});
|
||||
under(program_files, "Git/git-bash.exe"),
|
||||
under(program_files_x86, "Git/git-bash.exe"),
|
||||
under(user_profile, "scoop/apps/git/current/git-bash.exe"),
|
||||
}), {"--cd="});
|
||||
|
||||
add(ExternalApplicationId::wsl, "WSL", firstExisting({
|
||||
under(windows, "System32/wsl.exe"),
|
||||
}), {"--cd"});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Android
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
add(ExternalApplicationId::android_studio, "Android Studio", firstExisting({
|
||||
program_files / "Android/Android Studio/bin/studio64.exe",
|
||||
local / "Programs/Android Studio/bin/studio64.exe",
|
||||
}));
|
||||
under(program_files, "Android/Android Studio/bin/studio64.exe"),
|
||||
under(local, "Programs/Android Studio/bin/studio64.exe"),
|
||||
under(user_profile, "scoop/apps/android-studio/current/bin/studio64.exe"),
|
||||
}));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// JetBrains IDEs
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
std::filesystem::path idea = firstExisting({
|
||||
program_files / "JetBrains/IntelliJ IDEA 2025.1/bin/idea64.exe",
|
||||
program_files / "JetBrains/IntelliJ IDEA 2024.3/bin/idea64.exe",
|
||||
local / "Programs/IntelliJ IDEA Ultimate/bin/idea64.exe",
|
||||
under(program_files, "JetBrains/IntelliJ IDEA 2026.1/bin/idea64.exe"),
|
||||
under(program_files, "JetBrains/IntelliJ IDEA 2025.3/bin/idea64.exe"),
|
||||
under(program_files, "JetBrains/IntelliJ IDEA 2025.2/bin/idea64.exe"),
|
||||
under(program_files, "JetBrains/IntelliJ IDEA 2025.1/bin/idea64.exe"),
|
||||
under(program_files, "JetBrains/IntelliJ IDEA 2024.3/bin/idea64.exe"),
|
||||
|
||||
under(program_files, "JetBrains/IntelliJ IDEA Community Edition 2026.1/bin/idea64.exe"),
|
||||
under(program_files, "JetBrains/IntelliJ IDEA Community Edition 2025.3/bin/idea64.exe"),
|
||||
under(program_files, "JetBrains/IntelliJ IDEA Community Edition 2025.2/bin/idea64.exe"),
|
||||
under(program_files, "JetBrains/IntelliJ IDEA Community Edition 2025.1/bin/idea64.exe"),
|
||||
under(program_files, "JetBrains/IntelliJ IDEA Community Edition 2024.3/bin/idea64.exe"),
|
||||
|
||||
under(local, "Programs/IntelliJ IDEA Ultimate/bin/idea64.exe"),
|
||||
under(local, "Programs/IntelliJ IDEA Community/bin/idea64.exe"),
|
||||
});
|
||||
|
||||
if (idea.empty())
|
||||
idea = findExecutable(local / "JetBrains/Toolbox/apps", "idea64.exe");
|
||||
idea = findExecutable(under(local, "JetBrains/Toolbox/apps"), "idea64.exe");
|
||||
|
||||
add(ExternalApplicationId::intellij_idea, "IntelliJ IDEA", std::move(idea));
|
||||
|
||||
std::filesystem::path clion = firstExisting({
|
||||
under(program_files, "JetBrains/CLion 2026.1/bin/clion64.exe"),
|
||||
under(program_files, "JetBrains/CLion 2025.3/bin/clion64.exe"),
|
||||
under(program_files, "JetBrains/CLion 2025.2/bin/clion64.exe"),
|
||||
under(program_files, "JetBrains/CLion 2025.1/bin/clion64.exe"),
|
||||
under(program_files, "JetBrains/CLion 2024.3/bin/clion64.exe"),
|
||||
under(local, "Programs/CLion/bin/clion64.exe"),
|
||||
});
|
||||
|
||||
if (clion.empty())
|
||||
clion = findExecutable(under(local, "JetBrains/Toolbox/apps"), "clion64.exe");
|
||||
|
||||
add(ExternalApplicationId::clion, "CLion", std::move(clion));
|
||||
|
||||
std::filesystem::path rider = firstExisting({
|
||||
under(program_files, "JetBrains/JetBrains Rider 2026.1/bin/rider64.exe"),
|
||||
under(program_files, "JetBrains/JetBrains Rider 2025.3/bin/rider64.exe"),
|
||||
under(program_files, "JetBrains/JetBrains Rider 2025.2/bin/rider64.exe"),
|
||||
under(program_files, "JetBrains/JetBrains Rider 2025.1/bin/rider64.exe"),
|
||||
under(program_files, "JetBrains/JetBrains Rider 2024.3/bin/rider64.exe"),
|
||||
under(local, "Programs/Rider/bin/rider64.exe"),
|
||||
});
|
||||
|
||||
if (rider.empty())
|
||||
rider = findExecutable(under(local, "JetBrains/Toolbox/apps"), "rider64.exe");
|
||||
|
||||
add(ExternalApplicationId::rider, "Rider", std::move(rider));
|
||||
|
||||
std::filesystem::path pycharm = firstExisting({
|
||||
under(program_files, "JetBrains/PyCharm 2026.1/bin/pycharm64.exe"),
|
||||
under(program_files, "JetBrains/PyCharm 2025.3/bin/pycharm64.exe"),
|
||||
under(program_files, "JetBrains/PyCharm 2025.2/bin/pycharm64.exe"),
|
||||
under(program_files, "JetBrains/PyCharm 2025.1/bin/pycharm64.exe"),
|
||||
under(program_files, "JetBrains/PyCharm 2024.3/bin/pycharm64.exe"),
|
||||
|
||||
under(program_files, "JetBrains/PyCharm Community Edition 2026.1/bin/pycharm64.exe"),
|
||||
under(program_files, "JetBrains/PyCharm Community Edition 2025.3/bin/pycharm64.exe"),
|
||||
under(program_files, "JetBrains/PyCharm Community Edition 2025.2/bin/pycharm64.exe"),
|
||||
under(program_files, "JetBrains/PyCharm Community Edition 2025.1/bin/pycharm64.exe"),
|
||||
under(program_files, "JetBrains/PyCharm Community Edition 2024.3/bin/pycharm64.exe"),
|
||||
|
||||
under(local, "Programs/PyCharm Professional/bin/pycharm64.exe"),
|
||||
under(local, "Programs/PyCharm Community/bin/pycharm64.exe"),
|
||||
});
|
||||
|
||||
if (pycharm.empty())
|
||||
pycharm = findExecutable(under(local, "JetBrains/Toolbox/apps"), "pycharm64.exe");
|
||||
|
||||
add(ExternalApplicationId::pycharm, "PyCharm", std::move(pycharm));
|
||||
|
||||
std::filesystem::path webstorm = firstExisting({
|
||||
under(program_files, "JetBrains/WebStorm 2026.1/bin/webstorm64.exe"),
|
||||
under(program_files, "JetBrains/WebStorm 2025.3/bin/webstorm64.exe"),
|
||||
under(program_files, "JetBrains/WebStorm 2025.2/bin/webstorm64.exe"),
|
||||
under(program_files, "JetBrains/WebStorm 2025.1/bin/webstorm64.exe"),
|
||||
under(program_files, "JetBrains/WebStorm 2024.3/bin/webstorm64.exe"),
|
||||
under(local, "Programs/WebStorm/bin/webstorm64.exe"),
|
||||
});
|
||||
|
||||
if (webstorm.empty())
|
||||
webstorm = findExecutable(under(local, "JetBrains/Toolbox/apps"), "webstorm64.exe");
|
||||
|
||||
add(ExternalApplicationId::webstorm, "WebStorm", std::move(webstorm));
|
||||
|
||||
std::filesystem::path phpstorm = firstExisting({
|
||||
under(program_files, "JetBrains/PhpStorm 2026.1/bin/phpstorm64.exe"),
|
||||
under(program_files, "JetBrains/PhpStorm 2025.3/bin/phpstorm64.exe"),
|
||||
under(program_files, "JetBrains/PhpStorm 2025.2/bin/phpstorm64.exe"),
|
||||
under(program_files, "JetBrains/PhpStorm 2025.1/bin/phpstorm64.exe"),
|
||||
under(program_files, "JetBrains/PhpStorm 2024.3/bin/phpstorm64.exe"),
|
||||
under(local, "Programs/PhpStorm/bin/phpstorm64.exe"),
|
||||
});
|
||||
|
||||
if (phpstorm.empty())
|
||||
phpstorm = findExecutable(under(local, "JetBrains/Toolbox/apps"), "phpstorm64.exe");
|
||||
|
||||
add(ExternalApplicationId::phpstorm, "PhpStorm", std::move(phpstorm));
|
||||
|
||||
std::filesystem::path rubymine = firstExisting({
|
||||
under(program_files, "JetBrains/RubyMine 2026.1/bin/rubymine64.exe"),
|
||||
under(program_files, "JetBrains/RubyMine 2025.3/bin/rubymine64.exe"),
|
||||
under(program_files, "JetBrains/RubyMine 2025.2/bin/rubymine64.exe"),
|
||||
under(program_files, "JetBrains/RubyMine 2025.1/bin/rubymine64.exe"),
|
||||
under(program_files, "JetBrains/RubyMine 2024.3/bin/rubymine64.exe"),
|
||||
under(local, "Programs/RubyMine/bin/rubymine64.exe"),
|
||||
});
|
||||
|
||||
if (rubymine.empty())
|
||||
rubymine = findExecutable(under(local, "JetBrains/Toolbox/apps"), "rubymine64.exe");
|
||||
|
||||
add(ExternalApplicationId::rubymine, "RubyMine", std::move(rubymine));
|
||||
|
||||
std::filesystem::path goland = firstExisting({
|
||||
under(program_files, "JetBrains/GoLand 2026.1/bin/goland64.exe"),
|
||||
under(program_files, "JetBrains/GoLand 2025.3/bin/goland64.exe"),
|
||||
under(program_files, "JetBrains/GoLand 2025.2/bin/goland64.exe"),
|
||||
under(program_files, "JetBrains/GoLand 2025.1/bin/goland64.exe"),
|
||||
under(program_files, "JetBrains/GoLand 2024.3/bin/goland64.exe"),
|
||||
under(local, "Programs/GoLand/bin/goland64.exe"),
|
||||
});
|
||||
|
||||
if (goland.empty())
|
||||
goland = findExecutable(under(local, "JetBrains/Toolbox/apps"), "goland64.exe");
|
||||
|
||||
add(ExternalApplicationId::goland, "GoLand", std::move(goland));
|
||||
|
||||
std::filesystem::path datagrip = firstExisting({
|
||||
under(program_files, "JetBrains/DataGrip 2026.1/bin/datagrip64.exe"),
|
||||
under(program_files, "JetBrains/DataGrip 2025.3/bin/datagrip64.exe"),
|
||||
under(program_files, "JetBrains/DataGrip 2025.2/bin/datagrip64.exe"),
|
||||
under(program_files, "JetBrains/DataGrip 2025.1/bin/datagrip64.exe"),
|
||||
under(program_files, "JetBrains/DataGrip 2024.3/bin/datagrip64.exe"),
|
||||
under(local, "Programs/DataGrip/bin/datagrip64.exe"),
|
||||
});
|
||||
|
||||
if (datagrip.empty())
|
||||
datagrip = findExecutable(under(local, "JetBrains/Toolbox/apps"), "datagrip64.exe");
|
||||
|
||||
add(ExternalApplicationId::datagrip, "DataGrip", std::move(datagrip));
|
||||
|
||||
add(ExternalApplicationId::jetbrains_fleet, "JetBrains Fleet", firstExisting({
|
||||
under(local, "Programs/Fleet/Fleet.exe"),
|
||||
under(local, "JetBrains/Fleet/bin/Fleet.exe"),
|
||||
under(program_files, "JetBrains/Fleet/Fleet.exe"),
|
||||
}));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Lightweight / general editors
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
add(ExternalApplicationId::sublime_text, "Sublime Text", firstExisting({
|
||||
under(program_files, "Sublime Text/sublime_text.exe"),
|
||||
under(program_files, "Sublime Text 3/sublime_text.exe"),
|
||||
under(program_files_x86, "Sublime Text/sublime_text.exe"),
|
||||
under(local, "Programs/Sublime Text/sublime_text.exe"),
|
||||
under(user_profile, "scoop/apps/sublime-text/current/sublime_text.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::notepad_plus_plus, "Notepad++", firstExisting({
|
||||
under(program_files, "Notepad++/notepad++.exe"),
|
||||
under(program_files_x86, "Notepad++/notepad++.exe"),
|
||||
under(user_profile, "scoop/apps/notepadplusplus/current/notepad++.exe"),
|
||||
under(chocolatey, "lib/notepadplusplus/tools/notepad++.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::atom, "Atom", firstExisting({
|
||||
under(local, "atom/atom.exe"),
|
||||
under(local, "Programs/Atom/atom.exe"),
|
||||
under(program_files, "Atom/atom.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::pulsar, "Pulsar", firstExisting({
|
||||
under(local, "Programs/Pulsar/Pulsar.exe"),
|
||||
under(program_files, "Pulsar/Pulsar.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::brackets, "Brackets", firstExisting({
|
||||
under(program_files, "Brackets/Brackets.exe"),
|
||||
under(program_files_x86, "Brackets/Brackets.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::lapce, "Lapce", firstExisting({
|
||||
under(local, "Programs/Lapce/Lapce.exe"),
|
||||
under(program_files, "Lapce/Lapce.exe"),
|
||||
under(user_profile, "scoop/apps/lapce/current/lapce.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::lite_xl, "Lite XL", firstExisting({
|
||||
under(program_files, "Lite XL/lite-xl.exe"),
|
||||
under(program_files_x86, "Lite XL/lite-xl.exe"),
|
||||
under(user_profile, "scoop/apps/lite-xl/current/lite-xl.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::geany, "Geany", firstExisting({
|
||||
under(program_files, "Geany/bin/geany.exe"),
|
||||
under(program_files_x86, "Geany/bin/geany.exe"),
|
||||
under(user_profile, "scoop/apps/geany/current/bin/geany.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::kate, "Kate", firstExisting({
|
||||
under(program_files, "Kate/bin/kate.exe"),
|
||||
under(program_files_x86, "Kate/bin/kate.exe"),
|
||||
under(user_profile, "scoop/apps/kate/current/bin/kate.exe"),
|
||||
}));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// C/C++ / Java / general IDEs
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
add(ExternalApplicationId::qt_creator, "Qt Creator", firstExisting({
|
||||
under(program_files, "Qt/Tools/QtCreator/bin/qtcreator.exe"),
|
||||
under(program_files_x86, "Qt/Tools/QtCreator/bin/qtcreator.exe"),
|
||||
under(user_profile, "scoop/apps/qt-creator/current/bin/qtcreator.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::codeblocks, "Code::Blocks", firstExisting({
|
||||
under(program_files, "CodeBlocks/codeblocks.exe"),
|
||||
under(program_files_x86, "CodeBlocks/codeblocks.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::dev_cpp, "Dev-C++", firstExisting({
|
||||
under(program_files, "Embarcadero/Dev-Cpp/devcpp.exe"),
|
||||
under(program_files_x86, "Embarcadero/Dev-Cpp/devcpp.exe"),
|
||||
under(program_files, "Dev-Cpp/devcpp.exe"),
|
||||
under(program_files_x86, "Dev-Cpp/devcpp.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::eclipse, "Eclipse", firstExisting({
|
||||
under(user_profile, "eclipse/java-latest-released/eclipse/eclipse.exe"),
|
||||
under(program_files, "Eclipse Foundation/eclipse/eclipse.exe"),
|
||||
under(program_files, "Eclipse/eclipse.exe"),
|
||||
under(local, "Programs/Eclipse/eclipse.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::netbeans, "NetBeans", firstExisting({
|
||||
under(program_files, "NetBeans-25/netbeans/bin/netbeans64.exe"),
|
||||
under(program_files, "NetBeans-24/netbeans/bin/netbeans64.exe"),
|
||||
under(program_files, "NetBeans-23/netbeans/bin/netbeans64.exe"),
|
||||
under(program_files, "NetBeans/netbeans/bin/netbeans64.exe"),
|
||||
under(program_files_x86, "NetBeans/netbeans/bin/netbeans.exe"),
|
||||
}));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Terminal editors
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
add(ExternalApplicationId::vim, "Vim", firstExisting({
|
||||
under(program_files, "Vim/vim91/gvim.exe"),
|
||||
under(program_files, "Vim/vim90/gvim.exe"),
|
||||
under(program_files_x86, "Vim/vim91/gvim.exe"),
|
||||
under(program_files_x86, "Vim/vim90/gvim.exe"),
|
||||
under(user_profile, "scoop/apps/vim/current/gvim.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::neovim, "Neovim", firstExisting({
|
||||
under(program_files, "Neovim/bin/nvim-qt.exe"),
|
||||
under(program_files, "Neovim/bin/nvim.exe"),
|
||||
under(local, "Programs/Neovim/bin/nvim-qt.exe"),
|
||||
under(local, "Programs/Neovim/bin/nvim.exe"),
|
||||
under(user_profile, "scoop/apps/neovim/current/bin/nvim.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::emacs, "Emacs", firstExisting({
|
||||
under(program_files, "Emacs/x86_64/bin/runemacs.exe"),
|
||||
under(program_files, "Emacs/bin/runemacs.exe"),
|
||||
under(program_files_x86, "Emacs/bin/runemacs.exe"),
|
||||
under(user_profile, "scoop/apps/emacs/current/bin/runemacs.exe"),
|
||||
}));
|
||||
|
||||
add(ExternalApplicationId::helix, "Helix", firstExisting({
|
||||
under(program_files, "helix/hx.exe"),
|
||||
under(program_files_x86, "helix/hx.exe"),
|
||||
under(user_profile, "scoop/apps/helix/current/hx.exe"),
|
||||
}));
|
||||
}
|
||||
|
||||
const ExternalApplication *ApplicationManager::application(ExternalApplicationId id) const
|
||||
{
|
||||
const auto found = std::find_if(applications_.begin(), applications_.end(),
|
||||
[id](const ExternalApplication &application)
|
||||
{ return application.id == id; });
|
||||
{
|
||||
return application.id == id;
|
||||
});
|
||||
|
||||
return found == applications_.end() ? nullptr : &*found;
|
||||
}
|
||||
|
||||
ExternalApplicationId ApplicationManager::defaultApplication() const
|
||||
{
|
||||
const auto preferred = std::find_if(targets_.begin(), targets_.end(),
|
||||
[](const LaunchTarget &target)
|
||||
{
|
||||
return target.info.available &&
|
||||
target.info.id != ExternalApplicationId::file_explorer &&
|
||||
target.info.id != ExternalApplicationId::terminal &&
|
||||
target.info.id != ExternalApplicationId::git_bash &&
|
||||
target.info.id != ExternalApplicationId::wsl;
|
||||
});
|
||||
|
||||
if (preferred != targets_.end())
|
||||
return preferred->info.id;
|
||||
|
||||
const auto available = std::find_if(targets_.begin(), targets_.end(),
|
||||
[](const LaunchTarget &target)
|
||||
{ return target.info.available; });
|
||||
{
|
||||
return target.info.available;
|
||||
});
|
||||
|
||||
return available == targets_.end()
|
||||
? ExternalApplicationId::file_explorer
|
||||
: available->info.id;
|
||||
}
|
||||
|
||||
bool ApplicationManager::launch(ExternalApplicationId application,
|
||||
const std::filesystem::path &repository, std::string &error) const
|
||||
const std::filesystem::path &repository,
|
||||
std::string &error) const
|
||||
{
|
||||
const auto found = std::find_if(targets_.begin(), targets_.end(),
|
||||
[application](const LaunchTarget &target)
|
||||
{ return target.info.id == application; });
|
||||
{
|
||||
return target.info.id == application;
|
||||
});
|
||||
|
||||
if (found == targets_.end() || !found->info.available)
|
||||
{
|
||||
error = "The selected application is not installed";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::string> arguments = found->arguments;
|
||||
const std::string repository_path = repository.string();
|
||||
|
||||
if (application == ExternalApplicationId::git_bash && !arguments.empty())
|
||||
{
|
||||
arguments.back() += repository_path;
|
||||
}
|
||||
else
|
||||
{
|
||||
arguments.push_back(repository_path);
|
||||
}
|
||||
|
||||
const izo::ProcessResult result = izo::LaunchProcess({
|
||||
found->info.executable,
|
||||
std::move(arguments),
|
||||
repository,
|
||||
true,
|
||||
});
|
||||
|
||||
if (!result)
|
||||
{
|
||||
error = result.errorMessage.empty() ? "Unable to launch application" : result.errorMessage;
|
||||
return false;
|
||||
}
|
||||
|
||||
error = "Opened repository in " + found->info.name;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -7,15 +7,55 @@
|
||||
enum class ExternalApplicationId
|
||||
{
|
||||
visual_studio_code,
|
||||
visual_studio,
|
||||
visual_studio_code_insiders,
|
||||
vscodium,
|
||||
cursor,
|
||||
windsurf,
|
||||
trae,
|
||||
zed,
|
||||
antigravity,
|
||||
|
||||
visual_studio,
|
||||
|
||||
github_desktop,
|
||||
file_explorer,
|
||||
terminal,
|
||||
git_bash,
|
||||
wsl,
|
||||
|
||||
android_studio,
|
||||
|
||||
intellij_idea,
|
||||
clion,
|
||||
rider,
|
||||
pycharm,
|
||||
webstorm,
|
||||
phpstorm,
|
||||
rubymine,
|
||||
goland,
|
||||
datagrip,
|
||||
jetbrains_fleet,
|
||||
|
||||
sublime_text,
|
||||
notepad_plus_plus,
|
||||
atom,
|
||||
pulsar,
|
||||
brackets,
|
||||
lapce,
|
||||
lite_xl,
|
||||
geany,
|
||||
kate,
|
||||
|
||||
qt_creator,
|
||||
codeblocks,
|
||||
dev_cpp,
|
||||
eclipse,
|
||||
netbeans,
|
||||
|
||||
vim,
|
||||
neovim,
|
||||
emacs,
|
||||
helix,
|
||||
};
|
||||
|
||||
struct ExternalApplication
|
||||
@@ -46,4 +86,4 @@ private:
|
||||
|
||||
std::vector<LaunchTarget> targets_;
|
||||
std::vector<ExternalApplication> applications_;
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <git2.h>
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
@@ -85,6 +86,8 @@ struct RepositoryView {
|
||||
std::vector<WorkingFile> working_files;
|
||||
int selected_commit = 0;
|
||||
int scroll_to_commit = -1;
|
||||
std::chrono::steady_clock::time_point last_background_refresh{};
|
||||
bool pending_background_refresh = false;
|
||||
ToolbarHistoryAction undo_action;
|
||||
ToolbarHistoryAction redo_action;
|
||||
|
||||
|
||||
@@ -442,8 +442,8 @@ void drawMinimap(const std::vector<MinimapEntry>& entries, float scale,
|
||||
|
||||
void drawCodeLine(const std::string& text, SyntaxLanguage language, SyntaxState& syntax,
|
||||
float scale, ImU32 background = IM_COL32(0, 0, 0, 0), float left_gutter = 0.0f,
|
||||
float minimum_width = 0.0f) {
|
||||
const float row_height = scaled(21.0f, scale);
|
||||
float minimum_width = 0.0f, float custom_row_height = 0.0f) {
|
||||
const float row_height = custom_row_height > 0.0f ? custom_row_height : scaled(21.0f, scale);
|
||||
const ImVec2 start = ImGui::GetCursorScreenPos();
|
||||
const float width = std::max(minimum_width, ImGui::CalcTextSize(text.c_str()).x + left_gutter + scaled(12.0f, scale));
|
||||
ImGui::InvisibleButton("##code_line", {width, row_height});
|
||||
@@ -491,7 +491,7 @@ void DiffViewer::close() {
|
||||
hunks_.clear();
|
||||
file_lines_.clear();
|
||||
blame_lines_.clear();
|
||||
history_lines_.clear();
|
||||
history_entries_.clear();
|
||||
}
|
||||
|
||||
void DiffViewer::parseDiff(const std::string& text) {
|
||||
@@ -583,6 +583,30 @@ void DiffViewer::parseBlame(const std::string& text) {
|
||||
}
|
||||
}
|
||||
|
||||
void DiffViewer::parseHistory(const std::string& text) {
|
||||
history_entries_.clear();
|
||||
for (const std::string& line : splitLines(text)) {
|
||||
if (line.empty()) continue;
|
||||
HistoryEntry entry;
|
||||
size_t cursor = 0;
|
||||
auto consume_field = [&](std::string& out) {
|
||||
const size_t separator = line.find('\x1f', cursor);
|
||||
if (separator == std::string::npos) {
|
||||
out = line.substr(cursor);
|
||||
cursor = line.size();
|
||||
return;
|
||||
}
|
||||
out = line.substr(cursor, separator - cursor);
|
||||
cursor = separator + 1;
|
||||
};
|
||||
consume_field(entry.hash);
|
||||
consume_field(entry.date);
|
||||
consume_field(entry.author);
|
||||
consume_field(entry.summary);
|
||||
if (!entry.hash.empty() || !entry.summary.empty()) history_entries_.push_back(std::move(entry));
|
||||
}
|
||||
}
|
||||
|
||||
void DiffViewer::reload(RepositoryView& repository, GitManager& manager, std::string& notice) {
|
||||
std::vector<std::string> arguments;
|
||||
if (commit_id_.empty()) {
|
||||
@@ -620,7 +644,7 @@ void DiffViewer::reload(RepositoryView& repository, GitManager& manager, std::st
|
||||
}
|
||||
file_lines_.clear();
|
||||
blame_lines_.clear();
|
||||
history_lines_.clear();
|
||||
history_entries_.clear();
|
||||
if (mode_ != Mode::diff) loadSupplement(repository, manager, mode_, notice);
|
||||
}
|
||||
|
||||
@@ -649,17 +673,16 @@ void DiffViewer::loadSupplement(RepositoryView& repository, GitManager& manager,
|
||||
parseBlame(output);
|
||||
} else if (mode == Mode::history) {
|
||||
std::vector<std::string> arguments{
|
||||
"log", "--follow", "--date=short", "--pretty=format:%h %ad %an %s"};
|
||||
"log", "--follow", "--date=short", "--pretty=format:%h%x1f%ad%x1f%an%x1f%s"};
|
||||
if (!commit_id_.empty()) arguments.push_back(commit_id_);
|
||||
arguments.insert(arguments.end(), {"--", path_});
|
||||
if (!manager.captureGit(repository, arguments, output, error)) notice = error;
|
||||
history_lines_ = splitLines(output);
|
||||
parseHistory(output);
|
||||
}
|
||||
}
|
||||
|
||||
void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCache* avatars,
|
||||
float scale, ImFont* code_font, std::string& notice) {
|
||||
(void)avatars;
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {scaled(8, scale), scaled(5, scale)});
|
||||
ImGui::BeginChild("diff_viewer", {-1, -1}, ImGuiChildFlags_None,
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
@@ -713,7 +736,7 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCac
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
const bool show_minimap = mode_ == Mode::diff || mode_ == Mode::file;
|
||||
const bool show_minimap = mode_ == Mode::diff || mode_ == Mode::file || mode_ == Mode::blame || mode_ == Mode::history;
|
||||
const float minimap_width = scaled(56.0f, scale);
|
||||
float main_scroll_y = 0.0f;
|
||||
float main_window_height = 0.0f;
|
||||
@@ -736,10 +759,18 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCac
|
||||
}
|
||||
minimap_entries.push_back({0, IM_COL32(0, 0, 0, 0)});
|
||||
}
|
||||
} else {
|
||||
} else if (mode_ == Mode::file) {
|
||||
minimap_entries.reserve(file_lines_.size());
|
||||
for (const std::string& line : file_lines_)
|
||||
minimap_entries.push_back({line.size(), IM_COL32(112, 118, 128, 255)});
|
||||
} else if (mode_ == Mode::blame) {
|
||||
minimap_entries.reserve(blame_lines_.size());
|
||||
for (const BlameLine& line : blame_lines_)
|
||||
minimap_entries.push_back({line.text.size(), blameColor(line.hash, line.show_attribution ? 200 : 125)});
|
||||
} else {
|
||||
minimap_entries.reserve(history_entries_.size());
|
||||
for (const HistoryEntry& entry : history_entries_)
|
||||
minimap_entries.push_back({entry.summary.size() + entry.author.size(), IM_COL32(112, 118, 128, 255)});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -820,24 +851,57 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCac
|
||||
reload(repository, manager, notice);
|
||||
}
|
||||
} else if (mode_ == Mode::blame) {
|
||||
std::ostringstream blame_text;
|
||||
for (size_t index = 0; index < blame_lines_.size(); ++index) {
|
||||
const BlameLine& line = blame_lines_[index];
|
||||
if (index) blame_text << '\n';
|
||||
blame_text << line.line_number << " ";
|
||||
if (line.show_attribution) {
|
||||
blame_text << line.summary;
|
||||
if (!line.date.empty()) blame_text << " " << line.date;
|
||||
blame_text << " ";
|
||||
} else {
|
||||
blame_text << "| ";
|
||||
if (blame_lines_.empty()) {
|
||||
ImGui::TextDisabled("No blame data is available for this file.");
|
||||
} else {
|
||||
SyntaxState syntax;
|
||||
const float info_width = scaled(270.0f, scale);
|
||||
const float line_number_width = scaled(48.0f, scale);
|
||||
const float code_gutter = info_width + line_number_width + scaled(14.0f, scale);
|
||||
for (size_t index = 0; index < blame_lines_.size(); ++index) {
|
||||
ImGui::PushID(static_cast<int>(index));
|
||||
const BlameLine& line = blame_lines_[index];
|
||||
const ImVec2 line_minimum = ImGui::GetCursorScreenPos();
|
||||
const ImU32 accent = blameColor(line.hash, line.show_attribution ? 255 : 190);
|
||||
const ImU32 background = line.show_attribution ? IM_COL32(39, 42, 50, 150) : IM_COL32(31, 34, 40, 105);
|
||||
const float blame_row_height = line.show_attribution ? scaled(28.0f, scale) : scaled(21.0f, scale);
|
||||
drawCodeLine(line.text, language, syntax, scale, background, code_gutter, content_width,
|
||||
blame_row_height);
|
||||
ImDrawList* draw = ImGui::GetWindowDrawList();
|
||||
const ImVec2 line_maximum = ImGui::GetItemRectMax();
|
||||
draw->AddRectFilled({line_minimum.x, line_minimum.y},
|
||||
{line_minimum.x + scaled(3.0f, scale), line_maximum.y}, accent);
|
||||
drawCodeLineNumber(line.line_number, line_minimum.x + info_width + scaled(6.0f, scale),
|
||||
line_minimum.y + scaled(2.0f, scale), IM_COL32(126, 132, 142, 255));
|
||||
if (line.show_attribution) {
|
||||
const float avatar_size = scaled(16.0f, scale);
|
||||
const ImVec2 avatar_min{line_minimum.x + scaled(8.0f, scale), line_minimum.y + scaled(2.0f, scale)};
|
||||
const unsigned int avatar_texture = avatars ? avatars->textureFor(line.email) : 0;
|
||||
if (avatar_texture) {
|
||||
draw->AddImageRounded(ImTextureRef(static_cast<ImTextureID>(avatar_texture)),
|
||||
avatar_min, {avatar_min.x + avatar_size, avatar_min.y + avatar_size},
|
||||
{0, 0}, {1, 1}, IM_COL32_WHITE, scaled(3.0f, scale));
|
||||
} else {
|
||||
draw->AddRectFilled(avatar_min, {avatar_min.x + avatar_size, avatar_min.y + avatar_size},
|
||||
accent, scaled(3.0f, scale));
|
||||
}
|
||||
const float info_x = avatar_min.x + avatar_size + scaled(8.0f, scale);
|
||||
const std::string author = line.author.empty() ? "Unknown author" : line.author;
|
||||
std::string summary = line.summary.empty() ? "(no summary)" : line.summary;
|
||||
if (!line.date.empty()) summary += " " + line.date;
|
||||
draw->AddText({info_x, line_minimum.y + scaled(1.0f, scale)},
|
||||
IM_COL32(216, 220, 226, 255), author.c_str());
|
||||
draw->AddText({info_x, line_minimum.y + scaled(10.0f, scale)},
|
||||
IM_COL32(134, 140, 151, 255), summary.c_str());
|
||||
} else {
|
||||
draw->AddText({line_minimum.x + scaled(16.0f, scale), line_minimum.y + scaled(2.0f, scale)},
|
||||
IM_COL32(92, 99, 110, 255), "...");
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
blame_text << line.text;
|
||||
}
|
||||
drawSelectableTextBlock("##blame_text", blame_text.str(), {-1, -1});
|
||||
if (blame_lines_.empty()) ImGui::TextDisabled("No blame data is available for this file.");
|
||||
} else {
|
||||
const std::vector<std::string>* lines = mode_ == Mode::file ? &file_lines_ : &history_lines_;
|
||||
} else if (mode_ == Mode::file) {
|
||||
const std::vector<std::string>* lines = &file_lines_;
|
||||
if (mode_ == Mode::file) {
|
||||
SyntaxState syntax;
|
||||
const float code_gutter = scaled(52.0f, scale);
|
||||
@@ -850,10 +914,41 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCac
|
||||
line_minimum.y + scaled(2.0f, scale), IM_COL32(126, 132, 142, 255));
|
||||
ImGui::PopID();
|
||||
}
|
||||
} else {
|
||||
drawSelectableTextBlock("##history_text", joinLines(*lines), {-1, -1});
|
||||
}
|
||||
if (lines->empty()) ImGui::TextDisabled("No data is available for this view.");
|
||||
} else {
|
||||
if (history_entries_.empty()) {
|
||||
ImGui::TextDisabled("No history data is available for this file.");
|
||||
} else {
|
||||
for (size_t index = 0; index < history_entries_.size(); ++index) {
|
||||
ImGui::PushID(static_cast<int>(index));
|
||||
const HistoryEntry& entry = history_entries_[index];
|
||||
const ImVec2 row_min = ImGui::GetCursorScreenPos();
|
||||
const float history_row_height = scaled(36.0f, scale);
|
||||
ImGui::InvisibleButton("##history_row", {content_width, history_row_height});
|
||||
const ImVec2 row_max = ImGui::GetItemRectMax();
|
||||
ImDrawList* draw = ImGui::GetWindowDrawList();
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
draw->AddRectFilled(row_min, row_max,
|
||||
hovered ? IM_COL32(49, 53, 61, 180) : IM_COL32(37, 40, 47, 130), scaled(4.0f, scale));
|
||||
draw->AddRectFilled(row_min, {row_min.x + scaled(3.0f, scale), row_max.y},
|
||||
IM_COL32(23, 181, 204, 220), scaled(3.0f, scale));
|
||||
|
||||
const std::string hash_text = entry.hash.empty() ? "-" : entry.hash;
|
||||
draw->AddText({row_min.x + scaled(10.0f, scale), row_min.y + scaled(4.0f, scale)},
|
||||
IM_COL32(96, 184, 235, 255), hash_text.c_str());
|
||||
|
||||
const std::string author_date = entry.author +
|
||||
(entry.date.empty() ? std::string{} : std::string(" ") + entry.date);
|
||||
draw->AddText({row_min.x + scaled(78.0f, scale), row_min.y + scaled(4.0f, scale)},
|
||||
IM_COL32(188, 192, 198, 255), author_date.c_str());
|
||||
|
||||
const std::string summary = entry.summary.empty() ? "(no summary)" : entry.summary;
|
||||
draw->AddText({row_min.x + scaled(10.0f, scale), row_min.y + scaled(18.0f, scale)},
|
||||
IM_COL32(219, 223, 229, 255), summary.c_str());
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
}
|
||||
main_scroll_y = ImGui::GetScrollY();
|
||||
main_window_height = ImGui::GetWindowHeight();
|
||||
|
||||
@@ -44,6 +44,12 @@ private:
|
||||
int line_number = 0;
|
||||
bool show_attribution = false;
|
||||
};
|
||||
struct HistoryEntry {
|
||||
std::string hash;
|
||||
std::string date;
|
||||
std::string author;
|
||||
std::string summary;
|
||||
};
|
||||
|
||||
std::string path_;
|
||||
std::string commit_id_;
|
||||
@@ -53,10 +59,11 @@ private:
|
||||
std::vector<Hunk> hunks_;
|
||||
std::vector<std::string> file_lines_;
|
||||
std::vector<BlameLine> blame_lines_;
|
||||
std::vector<std::string> history_lines_;
|
||||
std::vector<HistoryEntry> history_entries_;
|
||||
|
||||
void reload(RepositoryView& repository, GitManager& manager, std::string& notice);
|
||||
void loadSupplement(RepositoryView& repository, GitManager& manager, Mode mode, std::string& notice);
|
||||
void parseDiff(const std::string& text);
|
||||
void parseBlame(const std::string& text);
|
||||
void parseHistory(const std::string& text);
|
||||
};
|
||||
|
||||
@@ -65,6 +65,7 @@ std::array<char, 128> g_create_repository_branch{};
|
||||
std::array<char, 1024> g_clone_repository_parent{};
|
||||
std::array<char, 1024> g_clone_repository_url{};
|
||||
std::string g_git_target;
|
||||
std::string g_git_target_label;
|
||||
std::array<char, 256> g_inline_branch_name{};
|
||||
std::string g_inline_branch_target;
|
||||
std::string g_requested_branch_checkout;
|
||||
@@ -115,6 +116,9 @@ float g_outline_icon_size = 15.0f;
|
||||
enum class ToolbarActionRequest { none, pull, push };
|
||||
ToolbarActionRequest g_pending_toolbar_action = ToolbarActionRequest::none;
|
||||
ToolbarActionRequest g_running_toolbar_action = ToolbarActionRequest::none;
|
||||
using RefreshClock = std::chrono::steady_clock;
|
||||
constexpr auto active_refresh_interval = std::chrono::seconds(2);
|
||||
constexpr auto background_refresh_interval = std::chrono::seconds(5);
|
||||
|
||||
|
||||
float ui(float value) { return value * g_ui_scale; }
|
||||
@@ -174,11 +178,45 @@ bool copy_to_clipboard(std::string_view text, const char* description) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void mark_repository_refreshed(RepositoryView& repository) {
|
||||
repository.last_background_refresh = RefreshClock::now();
|
||||
repository.pending_background_refresh = false;
|
||||
}
|
||||
|
||||
bool reload_repository_background(RepositoryView& repository, bool surface_errors = false) {
|
||||
if (!g_git_manager || !repository.repo || repository.path.empty()) return false;
|
||||
std::string error;
|
||||
repository.pending_background_refresh = false;
|
||||
repository.last_background_refresh = RefreshClock::now();
|
||||
if (!g_git_manager->reload(repository, error)) {
|
||||
if (surface_errors && !error.empty()) g_notice = error;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void process_repository_background_refreshes() {
|
||||
if (!g_git_manager || g_tabs.empty()) return;
|
||||
if (g_running_toolbar_action != ToolbarActionRequest::none) return;
|
||||
if (!g_running_branch_checkout.empty() || !g_requested_branch_checkout.empty()) return;
|
||||
const RefreshClock::time_point now = RefreshClock::now();
|
||||
for (size_t index = 0; index < g_tabs.size(); ++index) {
|
||||
RepositoryView& repository = *g_tabs[index];
|
||||
if (!repository.repo || repository.path.empty()) continue;
|
||||
const auto interval = index == g_active_tab ? active_refresh_interval : background_refresh_interval;
|
||||
const bool due = repository.pending_background_refresh ||
|
||||
repository.last_background_refresh.time_since_epoch().count() == 0 ||
|
||||
now - repository.last_background_refresh >= interval;
|
||||
if (due) reload_repository_background(repository, index == g_active_tab);
|
||||
}
|
||||
}
|
||||
|
||||
bool run_repository_action(const std::vector<std::string>& arguments, const std::string& success_notice,
|
||||
const std::string& focus_commit = {}) {
|
||||
std::string output;
|
||||
if (!g_git_manager->captureGit(repo(), arguments, output, g_notice)) return false;
|
||||
if (!g_git_manager->reload(repo(), g_notice)) return false;
|
||||
mark_repository_refreshed(repo());
|
||||
if (!focus_commit.empty()) g_pending_commit_jump = focus_commit;
|
||||
g_notice = success_notice;
|
||||
return true;
|
||||
@@ -294,6 +332,7 @@ void activate_repository_tab(size_t index) {
|
||||
g_active_tab = index;
|
||||
g_tab_to_select = g_tabs[index].get();
|
||||
reset_repository_view();
|
||||
g_tabs[index]->pending_background_refresh = true;
|
||||
persist_repository_session();
|
||||
}
|
||||
|
||||
@@ -321,6 +360,7 @@ void close_tab(size_t index) {
|
||||
bool open_repository(const char* path) {
|
||||
const bool opened = g_git_manager->openRepository(repo(), path, g_notice);
|
||||
if (opened) {
|
||||
mark_repository_refreshed(repo());
|
||||
if (g_user_data && path && *path) g_user_data->addRecentRepository(path);
|
||||
reset_repository_view();
|
||||
persist_repository_session();
|
||||
@@ -650,8 +690,8 @@ bool sidebar_section_header(const char* label, int count, const char* add_toolti
|
||||
if (section.find("LOCAL") != std::string::npos) {
|
||||
if (repo().commits.empty()) g_notice = "Create an initial commit before creating a branch";
|
||||
else {
|
||||
const int commit_index = repo().selected_commit >= 0 ? repo().selected_commit : 0;
|
||||
g_git_target = oid_string(repo().commits[static_cast<size_t>(commit_index)].oid);
|
||||
g_git_target = repo().branch;
|
||||
g_git_target_label = repo().branch.empty() ? "HEAD" : repo().branch;
|
||||
g_branch_create_popup = true;
|
||||
}
|
||||
}
|
||||
@@ -667,17 +707,19 @@ bool sidebar_section_header(const char* label, int count, const char* add_toolti
|
||||
return open;
|
||||
}
|
||||
|
||||
void sidebar_section_splitter(const char* id, size_t section_index, float maximum_height) {
|
||||
void sidebar_section_splitter(const char* id, size_t section_index, float current_height,
|
||||
float next_height, float minimum_height, size_t next_section_index, bool persist_next_height) {
|
||||
ImGui::PushID(id);
|
||||
ImGui::InvisibleButton("##vertical_resize", {-1, ui(5.0f)});
|
||||
const bool active = ImGui::IsItemActive();
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
if (active || hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS);
|
||||
if (active) {
|
||||
const float minimum_height = std::min(42.0f, maximum_height);
|
||||
const float height = std::clamp(g_user_data->sidebarSectionHeight(section_index) +
|
||||
ImGui::GetIO().MouseDelta.y / g_ui_scale, minimum_height, maximum_height);
|
||||
const float total_height = current_height + next_height;
|
||||
const float height = std::clamp(current_height + ImGui::GetIO().MouseDelta.y / g_ui_scale,
|
||||
minimum_height, std::max(minimum_height, total_height - minimum_height));
|
||||
g_user_data->setSidebarSectionHeight(section_index, height);
|
||||
if (persist_next_height) g_user_data->setSidebarSectionHeight(next_section_index, total_height - height);
|
||||
}
|
||||
if (ImGui::IsItemDeactivated()) g_user_data->save();
|
||||
const ImVec2 minimum = ImGui::GetItemRectMin();
|
||||
@@ -709,7 +751,8 @@ void sidebar_item_context(const std::string& item, SidebarItemKind kind) {
|
||||
|
||||
void section(const char* label, const std::vector<std::string>& items, const char* item_icon,
|
||||
const char* add_tooltip, const char* add_notice, SidebarItemKind kind, size_t section_index,
|
||||
float body_height, float maximum_height, bool resizable) {
|
||||
float body_height, float next_height, float minimum_height, size_t next_section_index,
|
||||
bool persist_next_height, bool resizable) {
|
||||
const bool open = sidebar_section_header(label, static_cast<int>(items.size()), add_tooltip, add_notice);
|
||||
if (open && body_height >= ui(1.0f)) {
|
||||
if (g_reset_repository_view) ImGui::SetNextWindowScroll({0.0f, 0.0f});
|
||||
@@ -747,7 +790,9 @@ void section(const char* label, const std::vector<std::string>& items, const cha
|
||||
}
|
||||
ImGui::Unindent(ui(sidebar_child_indent));
|
||||
ImGui::EndChild();
|
||||
if (resizable) sidebar_section_splitter(label, section_index, maximum_height);
|
||||
if (resizable)
|
||||
sidebar_section_splitter(label, section_index, body_height / g_ui_scale, next_height, minimum_height,
|
||||
next_section_index, persist_next_height);
|
||||
} else {
|
||||
ImGui::Separator();
|
||||
}
|
||||
@@ -843,7 +888,8 @@ void draw_branch_nodes(const BranchNode& parent, const char* branch_icon, const
|
||||
|
||||
void branch_section(const char* label, const std::vector<std::string>& branches, const char* branch_icon,
|
||||
const char* group_icon, const char* add_tooltip, const char* add_notice, bool remote,
|
||||
size_t section_index, float body_height, float maximum_height, bool resizable) {
|
||||
size_t section_index, float body_height, float next_height, float minimum_height, size_t next_section_index,
|
||||
bool persist_next_height, bool resizable) {
|
||||
const bool open = sidebar_section_header(label, static_cast<int>(branches.size()), add_tooltip, add_notice);
|
||||
if (open && body_height >= ui(1.0f)) {
|
||||
if (g_reset_repository_view) ImGui::SetNextWindowScroll({0.0f, 0.0f});
|
||||
@@ -857,7 +903,9 @@ void branch_section(const char* label, const std::vector<std::string>& branches,
|
||||
draw_branch_nodes(root, branch_icon, group_icon, remote, label);
|
||||
ImGui::Unindent(ui(sidebar_child_indent));
|
||||
ImGui::EndChild();
|
||||
if (resizable) sidebar_section_splitter(label, section_index, maximum_height);
|
||||
if (resizable)
|
||||
sidebar_section_splitter(label, section_index, body_height / g_ui_scale, next_height, minimum_height,
|
||||
next_section_index, persist_next_height);
|
||||
} else {
|
||||
ImGui::Separator();
|
||||
}
|
||||
@@ -982,9 +1030,12 @@ void draw_sidebar(float width) {
|
||||
: 0.0f;
|
||||
const float body_space = std::max(0.0f, ImGui::GetContentRegionAvail().y - header_space - splitter_space);
|
||||
std::array<float, 4> section_heights{};
|
||||
std::array<float, 4> maximum_heights{};
|
||||
std::array<size_t, 4> next_open_indices{};
|
||||
next_open_indices.fill(section_open.size());
|
||||
float minimum_height = 0.0f;
|
||||
if (!open_indices.empty()) {
|
||||
const float minimum_body = std::min(ui(42.0f), body_space / static_cast<float>(open_indices.size()));
|
||||
minimum_height = minimum_body / g_ui_scale;
|
||||
float remaining = body_space;
|
||||
for (size_t position = 0; position + 1 < open_indices.size(); ++position) {
|
||||
const size_t index = open_indices[position];
|
||||
@@ -994,30 +1045,27 @@ void draw_sidebar(float width) {
|
||||
section_heights[index] = std::clamp(
|
||||
ui(g_user_data->sidebarSectionHeight(index)), minimum_body, maximum);
|
||||
remaining -= section_heights[index];
|
||||
next_open_indices[index] = open_indices[position + 1];
|
||||
}
|
||||
section_heights[last_open] = std::max(0.0f, remaining);
|
||||
for (size_t position = 0; position + 1 < open_indices.size(); ++position) {
|
||||
const size_t index = open_indices[position];
|
||||
float other_heights = 0.0f;
|
||||
for (size_t other_position = 0; other_position + 1 < open_indices.size(); ++other_position)
|
||||
if (other_position != position) other_heights += section_heights[open_indices[other_position]];
|
||||
maximum_heights[index] = std::max(minimum_body,
|
||||
body_space - other_heights - minimum_body) / g_ui_scale;
|
||||
}
|
||||
}
|
||||
|
||||
branch_section(ICON_TB_DESKTOP " LOCAL", repo().local_branches, ICON_TB_CODE_BRANCH, ICON_TB_FOLDER,
|
||||
"Create local branch", "Create local branch", false, 0, section_heights[0], maximum_heights[0],
|
||||
section_open[0] && last_open != 0);
|
||||
"Create local branch", "Create local branch", false, 0, section_heights[0],
|
||||
next_open_indices[0] < section_open.size() ? section_heights[next_open_indices[0]] / g_ui_scale : 0.0f,
|
||||
minimum_height, next_open_indices[0], next_open_indices[0] != last_open, section_open[0] && last_open != 0);
|
||||
branch_section(ICON_TB_CLOUD " REMOTE", repo().remote_branches, ICON_TB_CODE_BRANCH, ICON_TB_GLOBE,
|
||||
"Add remote", "Add remote", true, 1, section_heights[1], maximum_heights[1],
|
||||
section_open[1] && last_open != 1);
|
||||
"Add remote", "Add remote", true, 1, section_heights[1],
|
||||
next_open_indices[1] < section_open.size() ? section_heights[next_open_indices[1]] / g_ui_scale : 0.0f,
|
||||
minimum_height, next_open_indices[1], next_open_indices[1] != last_open, section_open[1] && last_open != 1);
|
||||
section(ICON_TB_TREE " WORKTREES", repo().worktrees, ICON_TB_COMPUTER,
|
||||
"Add worktree", "Add worktree", SidebarItemKind::worktree, 2,
|
||||
section_heights[2], maximum_heights[2], section_open[2] && last_open != 2);
|
||||
section_heights[2],
|
||||
next_open_indices[2] < section_open.size() ? section_heights[next_open_indices[2]] / g_ui_scale : 0.0f,
|
||||
minimum_height, next_open_indices[2], next_open_indices[2] != last_open, section_open[2] && last_open != 2);
|
||||
section(ICON_TB_LAYERS_LINKED " SUBMODULES", repo().submodules, ICON_TB_LAYERS_LINKED,
|
||||
"Add submodule", "Add submodule", SidebarItemKind::submodule, 3,
|
||||
section_heights[3], maximum_heights[3], false);
|
||||
section_heights[3], 0.0f, minimum_height, section_open.size(), false, false);
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::EndChild();
|
||||
ImGui::EndChild();
|
||||
@@ -1385,6 +1433,7 @@ void draw_commit_table() {
|
||||
};
|
||||
if (!repo().working_files.empty()) {
|
||||
ImGui::TableNextRow(0, ui(24.0f));
|
||||
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, IM_COL32(34, 37, 44, 42));
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
bool pending_hovered = false;
|
||||
if (graph_row_interaction("##working_tree", repo().selected_commit == -1,
|
||||
@@ -1465,6 +1514,8 @@ void draw_commit_table() {
|
||||
if (!commit_visible(commit)) continue;
|
||||
const float row_height = row_heights[static_cast<size_t>(i)];
|
||||
ImGui::TableNextRow(0, row_height);
|
||||
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,
|
||||
(i & 1) == 0 ? IM_COL32(35, 38, 45, 34) : IM_COL32(32, 35, 41, 18));
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
if (repo().scroll_to_commit == i) {
|
||||
ImGui::SetScrollFromPosY(ImGui::GetCursorScreenPos().y, 0.5f);
|
||||
@@ -1505,6 +1556,7 @@ void draw_commit_table() {
|
||||
}
|
||||
if (ImGui::MenuItem(ICON_TB_CODE_BRANCH " Create branch here")) {
|
||||
g_git_target = commit_hash;
|
||||
g_git_target_label = std::string("commit ") + commit.short_id;
|
||||
g_branch_create_popup = true;
|
||||
}
|
||||
ImGui::Separator();
|
||||
@@ -2599,6 +2651,9 @@ void draw_git_action_popups() {
|
||||
const bool submit_with_enter = ImGui::InputText("##local_branch_name",
|
||||
g_git_name.data(), g_git_name.size(), ImGuiInputTextFlags_EnterReturnsTrue);
|
||||
text_height_checkbox("Check out after creating", &checkout_new_branch);
|
||||
ImGui::TextDisabled("Branching off: %s", g_git_target_label.empty()
|
||||
? (repo().branch.empty() ? "HEAD" : repo().branch.c_str())
|
||||
: g_git_target_label.c_str());
|
||||
ImGui::TextDisabled("Start point: %.12s", g_git_target.empty() ? "HEAD" : g_git_target.c_str());
|
||||
const bool can_submit = g_git_name[0] != '\0';
|
||||
ImGui::BeginDisabled(!can_submit);
|
||||
@@ -2606,12 +2661,14 @@ void draw_git_action_popups() {
|
||||
if (submit && can_submit && g_git_manager->createBranch(repo(), g_git_name.data(),
|
||||
g_git_target, checkout_new_branch, g_notice)) {
|
||||
g_git_target.clear();
|
||||
g_git_target_label.clear();
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel", {ui(90.0f), 0})) {
|
||||
g_git_target.clear();
|
||||
g_git_target_label.clear();
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
@@ -2826,7 +2883,7 @@ void draw_popups() {
|
||||
std::string output;
|
||||
g_git_manager->captureGit(repo(), {"lfs", "install", "--local"}, output, g_notice);
|
||||
}
|
||||
g_git_manager->reload(repo(), g_notice);
|
||||
if (g_git_manager->reload(repo(), g_notice)) mark_repository_refreshed(repo());
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
@@ -3068,7 +3125,8 @@ void draw_app() {
|
||||
}
|
||||
if (ImGui::BeginMenu("Edit")) { ImGui::MenuItem("Preferences", nullptr, false, false); ImGui::EndMenu(); }
|
||||
if (ImGui::BeginMenu("View")) {
|
||||
if (ImGui::MenuItem("Refresh", "F5")) g_git_manager->reload(repo(), g_notice);
|
||||
if (ImGui::MenuItem("Refresh", "F5") && g_git_manager->reload(repo(), g_notice))
|
||||
mark_repository_refreshed(repo());
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
if (ImGui::BeginMenu("Help")) {
|
||||
@@ -3413,7 +3471,8 @@ int runGitree(int argc, char** argv) {
|
||||
} else if (!user_data.openRepositories().empty()) {
|
||||
for (const std::string& path : user_data.openRepositories()) {
|
||||
g_tabs.push_back(std::make_unique<RepositoryView>());
|
||||
if (!path.empty()) git_manager.openRepository(*g_tabs.back(), path, g_notice);
|
||||
if (!path.empty() && git_manager.openRepository(*g_tabs.back(), path, g_notice))
|
||||
mark_repository_refreshed(*g_tabs.back());
|
||||
}
|
||||
g_active_tab = std::min(user_data.activeRepository(), g_tabs.size() - 1);
|
||||
g_tab_to_select = g_tabs[g_active_tab].get();
|
||||
@@ -3439,6 +3498,7 @@ int runGitree(int argc, char** argv) {
|
||||
|
||||
while (!window_manager.shouldClose()) {
|
||||
window_manager.pollEvents();
|
||||
process_repository_background_refreshes();
|
||||
const bool dpi_changed = window_manager.consumeDpiChange();
|
||||
if (dpi_changed || g_zoom_reload_requested) {
|
||||
load_fonts(combined_ui_scale(window_manager.dpiScale(), g_zoom_percent));
|
||||
|
||||
Reference in New Issue
Block a user