feat(toolbar): add external application launcher

This commit is contained in:
2026-06-18 20:02:49 -05:00
parent 9c64c74f86
commit 23a60061f6
4 changed files with 241 additions and 0 deletions

View File

@@ -70,6 +70,8 @@ add_executable(gitree WIN32
src/managers/user_data.h
src/managers/avatar_cache.cpp
src/managers/avatar_cache.h
src/managers/application_manager.cpp
src/managers/application_manager.h
src/models/repository.h
)
target_include_directories(gitree PRIVATE src vendor/libgit2/include vendor/icons)

View File

@@ -0,0 +1,122 @@
#include "managers/application_manager.h"
#include <izo/Process.hpp>
#include <algorithm>
#include <cstdlib>
namespace {
std::filesystem::path environmentPath(const char* name) {
const char* value = std::getenv(name);
return value && *value ? 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)
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;
iterator != end && !error; iterator.increment(error)) {
if (iterator->is_regular_file(error) && iterator->path().filename() == filename)
return iterator->path();
}
return {};
}
}
ApplicationManager::ApplicationManager() {
const auto local = environmentPath("LOCALAPPDATA");
const auto program_files = environmentPath("ProgramFiles");
const auto program_files_x86 = environmentPath("ProgramFiles(x86)");
const auto windows = environmentPath("WINDIR");
auto add = [this](ExternalApplicationId id, const char* name, std::filesystem::path executable,
std::vector<std::string> arguments = {}) {
const bool available = !executable.empty();
targets_.push_back({{id, name, available}, std::move(executable), std::move(arguments)});
applications_.push_back(targets_.back().info);
};
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",
}));
add(ExternalApplicationId::antigravity, "Antigravity", firstExisting({
local / "Programs/Antigravity/Antigravity.exe",
local / "Antigravity/Antigravity.exe",
program_files / "Antigravity/Antigravity.exe",
}));
std::filesystem::path github_desktop = firstExisting({
local / "GitHubDesktop/GitHubDesktop.exe",
local / "GitHub Desktop/GitHubDesktop.exe",
});
if (github_desktop.empty())
github_desktop = findExecutable(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::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",
}));
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",
});
if (idea.empty()) idea = findExecutable(local / "JetBrains/Toolbox/apps", "idea64.exe");
add(ExternalApplicationId::intellij_idea, "IntelliJ IDEA", std::move(idea));
}
ExternalApplicationId ApplicationManager::defaultApplication() const {
const auto available = std::find_if(targets_.begin(), targets_.end(),
[](const LaunchTarget& target) { 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 auto found = std::find_if(targets_.begin(), targets_.end(),
[application](const LaunchTarget& target) { 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->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;
}

View File

@@ -0,0 +1,44 @@
#pragma once
#include <filesystem>
#include <string>
#include <vector>
enum class ExternalApplicationId {
visual_studio_code,
visual_studio,
antigravity,
github_desktop,
file_explorer,
terminal,
git_bash,
wsl,
android_studio,
intellij_idea,
};
struct ExternalApplication {
ExternalApplicationId id;
std::string name;
bool available = false;
};
class ApplicationManager {
public:
ApplicationManager();
const std::vector<ExternalApplication>& applications() const { return applications_; }
ExternalApplicationId defaultApplication() const;
bool launch(ExternalApplicationId application, const std::filesystem::path& repository,
std::string& error) const;
private:
struct LaunchTarget {
ExternalApplication info;
std::filesystem::path executable;
std::vector<std::string> arguments;
};
std::vector<LaunchTarget> targets_;
std::vector<ExternalApplication> applications_;
};

View File

@@ -6,6 +6,7 @@
#include <izo/Dialogs.hpp>
#include <IconsFontAwesome6.h>
#include "ui/gitree_ui.h"
#include "managers/application_manager.h"
#include "managers/avatar_cache.h"
#include "managers/git_manager.h"
#include "managers/user_data.h"
@@ -69,6 +70,8 @@ float g_details_width = 368.0f;
DiffViewer g_diff_viewer;
WindowManager* g_window_manager = nullptr;
GitManager* g_git_manager = nullptr;
ApplicationManager* g_application_manager = nullptr;
ExternalApplicationId g_open_in_application = ExternalApplicationId::visual_studio_code;
ImFont* g_outline_icon_font = nullptr;
float g_outline_icon_size = 15.0f;
@@ -1276,6 +1279,71 @@ bool toolbar_selector(const char* id, const char* label, const std::string& valu
return clicked;
}
const char* application_icon(ExternalApplicationId application) {
switch (application) {
case ExternalApplicationId::visual_studio_code: return ICON_FA_CODE;
case ExternalApplicationId::visual_studio: return ICON_FA_CUBES;
case ExternalApplicationId::antigravity: return ICON_FA_WINDOW_MAXIMIZE;
case ExternalApplicationId::github_desktop: return ICON_FA_CIRCLE_NODES;
case ExternalApplicationId::file_explorer: return ICON_FA_FOLDER;
case ExternalApplicationId::terminal: return ICON_FA_TERMINAL;
case ExternalApplicationId::git_bash: return ICON_FA_CODE_BRANCH;
case ExternalApplicationId::wsl: return ICON_FA_SERVER;
case ExternalApplicationId::android_studio: return ICON_FA_ROBOT;
case ExternalApplicationId::intellij_idea: return ICON_FA_JET_FIGHTER_UP;
}
return ICON_FA_ARROW_UP_RIGHT_FROM_SQUARE;
}
void open_repository_in(ExternalApplicationId application) {
if (!g_application_manager) return;
g_open_in_application = application;
g_application_manager->launch(application, repo().path, g_notice);
}
void draw_open_in_button() {
if (!g_application_manager) return;
constexpr float button_width = 58.0f;
ImGui::SetCursorPos({ImGui::GetWindowWidth() - ui(button_width + 7.0f), ui(5.0f)});
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {0, 0});
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, ui(5.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, ui(1.0f));
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.12f, 0.14f, 0.17f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.18f, 0.21f, 0.25f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.25f, 0.28f, 0.33f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.20f, 0.68f, 0.95f, 1.0f));
if (ImGui::Button((std::string(application_icon(g_open_in_application)) + "##open_in").c_str(),
{ui(37.0f), ui(31.0f)}))
open_repository_in(g_open_in_application);
if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) ImGui::SetTooltip("Open repository");
ImGui::SameLine(0, 0);
ImGui::PopStyleColor();
if (ImGui::Button(ICON_FA_CHEVRON_DOWN "##open_in_menu", {ui(21.0f), ui(31.0f)}))
ImGui::OpenPopup("open_repository_in");
ImGui::PopStyleColor(3);
ImGui::PopStyleVar(3);
ImGui::SetNextWindowSize({ui(230.0f), 0}, ImGuiCond_Appearing);
if (ImGui::BeginPopup("open_repository_in")) {
ImGui::TextDisabled("OPEN REPOSITORY IN");
ImGui::Separator();
for (const ExternalApplication& application : g_application_manager->applications()) {
ImGui::PushID(static_cast<int>(application.id));
ImGui::BeginDisabled(!application.available);
const std::string label = std::string(application_icon(application.id)) + " " + application.name;
if (ImGui::MenuItem(label.c_str(), nullptr, application.id == g_open_in_application)) {
open_repository_in(application.id);
ImGui::CloseCurrentPopup();
}
ImGui::EndDisabled();
if (!application.available && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
ImGui::SetTooltip("Not installed");
ImGui::PopID();
}
ImGui::EndPopup();
}
}
bool contains_case_insensitive(const std::string& text, const char* query) {
if (!query || !*query) return true;
std::string lowered_text = text;
@@ -1756,6 +1824,7 @@ void draw_app() {
ImGui::SameLine(0, action_spacing);
if (toolbar_action("pop", "Pop", ICON_FA_BOX_OPEN, "Pop latest stash", true, false, 54))
g_git_manager->popStash(repo(), g_notice);
draw_open_in_button();
ImGui::EndChild();
ImGui::PopStyleColor();
@@ -1814,6 +1883,9 @@ void draw_app() {
int runGitree(int argc, char** argv) {
GitManager git_manager;
g_git_manager = &git_manager;
ApplicationManager application_manager;
g_application_manager = &application_manager;
g_open_in_application = application_manager.defaultApplication();
UserData user_data;
g_user_data = &user_data;
g_sidebar_width = user_data.sidebarWidth();
@@ -1867,6 +1939,7 @@ int runGitree(int argc, char** argv) {
avatar_cache.shutdown();
g_avatar_cache = nullptr;
g_application_manager = nullptr;
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
user_data.setSidebarWidth(g_sidebar_width);