feat(toolbar): add external application launcher
This commit is contained in:
@@ -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)
|
||||
|
||||
122
src/managers/application_manager.cpp
Normal file
122
src/managers/application_manager.cpp
Normal 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;
|
||||
}
|
||||
44
src/managers/application_manager.h
Normal file
44
src/managers/application_manager.h
Normal 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_;
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user