initial git repo
This commit is contained in:
gofnnp 2025-12-22 21:42:47 +04:00
commit 6e1e41799e
6 changed files with 642 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.vscode
build

23
CMakeLists.txt Normal file
View File

@ -0,0 +1,23 @@
cmake_minimum_required(VERSION 3.10)
project(project-launcher)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK4 REQUIRED gtk4)
find_package(nlohmann_json 3.2.0 REQUIRED)
include_directories(${GTK4_INCLUDE_DIRS})
link_directories(${GTK4_LIBRARY_DIRS})
add_executable(project-launcher
src/main.cpp
src/project_manager.cpp
)
target_link_libraries(project-launcher
PRIVATE
${GTK4_LIBRARIES}
nlohmann_json::nlohmann_json
)

103
resources/style.css Normal file
View File

@ -0,0 +1,103 @@
/* * {
background: red;
} */
window {
background-color: #181825;
color: #ffffff;
}
box {
background: #181825;
}
.project-name {
font-weight: bold;
font-size: 14px;
color: #ffffff;
}
.project-path {
font-size: 12px;
color: #888888;
}
.circular {
padding: 5px;
border-radius: 50%;
background: alpha(currentColor, 0.1);
}
.circular:hover {
background: alpha(currentColor, 0.2);
}
listboxrow {
padding: 8px;
border-radius: 6px;
margin: 2px 0;
background: #2d2d2d;
}
listboxrow:hover {
background: #3d3d3d;
}
/* listboxrow box {
background: transparent;
} */
entry {
background: #2d2d2d;
border: 1px solid #3d3d3d;
border-radius: 6px;
color: #ffffff;
padding: 8px;
margin: 0;
}
button {
background: #2d2d2d;
border: none;
border-radius: 6px;
color: #ffffff;
padding: 8px 16px;
margin: 0;
}
button:hover {
background: #3d3d3d;
}
button.suggested-action {
background: #2864b4;
color: #ffffff;
}
button.suggested-action:hover {
background: #3275c5;
}
button.destructive-action {
background: #c01c28;
color: #ffffff;
}
button.destructive-action:hover {
background: #d1293a;
}
.list-box {
background-color: #181825;
}
.list-box row {
background-color: #181825;
border-radius: 6px;
margin: 8px;
padding: 8px 4px;
}
.list-box row:selected {
outline: #2864b4 solid 2px;
}

375
src/main.cpp Normal file
View File

@ -0,0 +1,375 @@
#include <gtk/gtk.h>
#include "project_manager.hpp"
#include <memory>
struct AppData
{
ProjectManager *project_manager;
GtkListBox *project_list;
GtkWindow *main_window;
GtkApplication *app;
};
static GtkWidget *create_window(const char *title, GtkWindow *parent);
static void update_project_list(AppData *data);
static void on_project_activated(GtkListBox *box, GtkListBoxRow *row, AppData *data)
{
int index = gtk_list_box_row_get_index(row);
// Открываем проект
if (data->project_manager->openProject(index))
{
// Добавляем небольшую задержку перед закрытием
g_timeout_add(100, [](gpointer user_data) -> gboolean {
auto *app = static_cast<GtkApplication*>(user_data);
g_application_quit(G_APPLICATION(app));
return G_SOURCE_REMOVE;
}, data->app);
}
}
static void on_dialog_response(GtkButton *button, AppData *data)
{
GtkWidget *dialog = gtk_widget_get_ancestor(GTK_WIDGET(button), GTK_TYPE_WINDOW);
GtkWidget *main_box = gtk_window_get_child(GTK_WINDOW(dialog));
GtkWidget *name_entry = gtk_widget_get_first_child(main_box);
GtkWidget *path_entry = gtk_widget_get_next_sibling(name_entry);
const char *name = gtk_editable_get_text(GTK_EDITABLE(name_entry));
const char *path = gtk_editable_get_text(GTK_EDITABLE(path_entry));
if (strlen(name) > 0 && strlen(path) > 0)
{
data->project_manager->addProject(name, path);
update_project_list(data);
}
gtk_window_destroy(GTK_WINDOW(dialog));
}
static void on_add_clicked(GtkButton *button, AppData *data)
{
// Создаем диалог
GtkWidget *dialog = create_window("Добавить проект", data->main_window);
// Создаем основной контейнер
GtkWidget *main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
gtk_window_set_child(GTK_WINDOW(dialog), main_box);
// Добавляем отступы
gtk_widget_set_margin_start(main_box, 10);
gtk_widget_set_margin_end(main_box, 10);
gtk_widget_set_margin_top(main_box, 10);
gtk_widget_set_margin_bottom(main_box, 10);
// Создаем поля ввода
GtkWidget *name_entry = gtk_entry_new();
gtk_entry_set_placeholder_text(GTK_ENTRY(name_entry), "Название проекта");
gtk_box_append(GTK_BOX(main_box), name_entry);
GtkWidget *path_entry = gtk_entry_new();
gtk_entry_set_placeholder_text(GTK_ENTRY(path_entry), "Путь к проекту");
gtk_box_append(GTK_BOX(main_box), path_entry);
// Создаем контейнер для кнопок
GtkWidget *button_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
gtk_widget_set_halign(button_box, GTK_ALIGN_END);
gtk_box_append(GTK_BOX(main_box), button_box);
// Добавляем кнопки
GtkWidget *cancel_button = gtk_button_new_with_label("Отмена");
GtkWidget *accept_button = gtk_button_new_with_label("Добавить");
gtk_box_append(GTK_BOX(button_box), cancel_button);
gtk_box_append(GTK_BOX(button_box), accept_button);
// Подключаем сигналы для кнопок
g_signal_connect_swapped(cancel_button, "clicked", G_CALLBACK(gtk_window_destroy), dialog);
g_signal_connect(accept_button, "clicked", G_CALLBACK(on_dialog_response), data);
// Показываем диалог
gtk_window_present(GTK_WINDOW(dialog));
}
static void on_delete_clicked(GtkButton *button, AppData *data)
{
GtkWidget *row = gtk_widget_get_ancestor(GTK_WIDGET(button), GTK_TYPE_LIST_BOX_ROW);
int index = gtk_list_box_row_get_index(GTK_LIST_BOX_ROW(row));
data->project_manager->deleteProject(index);
update_project_list(data);
}
static void on_save_clicked(GtkButton *button, gpointer user_data)
{
auto *data = static_cast<std::pair<AppData *, int> *>(user_data);
GtkWidget *dialog = gtk_widget_get_ancestor(GTK_WIDGET(button), GTK_TYPE_WINDOW);
GtkWidget *box = gtk_window_get_child(GTK_WINDOW(dialog));
GtkWidget *name_entry = gtk_widget_get_first_child(box);
GtkWidget *path_entry = gtk_widget_get_next_sibling(name_entry);
const char *name = gtk_editable_get_text(GTK_EDITABLE(name_entry));
const char *path = gtk_editable_get_text(GTK_EDITABLE(path_entry));
if (strlen(name) > 0 && strlen(path) > 0)
{
data->first->project_manager->editProject(data->second, name, path);
update_project_list(data->first);
}
gtk_window_destroy(GTK_WINDOW(dialog));
delete data;
}
static void on_edit_clicked(GtkButton *button, AppData *data)
{
GtkWidget *row = gtk_widget_get_ancestor(GTK_WIDGET(button), GTK_TYPE_LIST_BOX_ROW);
int index = gtk_list_box_row_get_index(GTK_LIST_BOX_ROW(row));
const auto &projects = data->project_manager->getProjects();
const auto &project = projects[index];
// Создаем диалог
GtkWidget *dialog = create_window("Редактировать проект", data->main_window);
// Создаем основной контейнер
GtkWidget *main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
gtk_window_set_child(GTK_WINDOW(dialog), main_box);
gtk_widget_set_margin_start(main_box, 10);
gtk_widget_set_margin_end(main_box, 10);
gtk_widget_set_margin_top(main_box, 10);
gtk_widget_set_margin_bottom(main_box, 10);
// Создаем поля ввода
GtkWidget *name_entry = gtk_entry_new();
gtk_editable_set_text(GTK_EDITABLE(name_entry), project.name.c_str());
gtk_box_append(GTK_BOX(main_box), name_entry);
GtkWidget *path_entry = gtk_entry_new();
gtk_editable_set_text(GTK_EDITABLE(path_entry), project.path.c_str());
gtk_box_append(GTK_BOX(main_box), path_entry);
// Создаем контейнер для кнопок
GtkWidget *button_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
gtk_widget_set_halign(button_box, GTK_ALIGN_END);
gtk_box_append(GTK_BOX(main_box), button_box);
// Добавляем кнопки
GtkWidget *cancel_button = gtk_button_new_with_label("Отмена");
GtkWidget *save_button = gtk_button_new_with_label("Сохранить");
gtk_widget_add_css_class(save_button, "suggested-action");
gtk_box_append(GTK_BOX(button_box), cancel_button);
gtk_box_append(GTK_BOX(button_box), save_button);
// Подключаем сигналы
g_signal_connect_swapped(cancel_button, "clicked", G_CALLBACK(gtk_window_destroy), dialog);
auto *user_data = new std::pair<AppData *, int>(data, index);
g_signal_connect(save_button, "clicked", G_CALLBACK(on_save_clicked), user_data);
gtk_window_present(GTK_WINDOW(dialog));
}
static void on_search_changed(GtkSearchEntry *entry, AppData *data)
{
gtk_list_box_invalidate_filter(data->project_list);
}
static void update_project_list(AppData *data)
{
// Очищаем текущий список
GtkWidget *child;
while ((child = gtk_widget_get_first_child(GTK_WIDGET(data->project_list))) != NULL)
{
gtk_list_box_remove(data->project_list, GTK_WIDGET(child));
}
// Добавляем проекты
for (const auto &project : data->project_manager->getProjects())
{
// Создаем основной контейнер для элемента списка
GtkWidget *row_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
gtk_widget_set_margin_start(row_box, 10);
gtk_widget_set_margin_end(row_box, 10);
gtk_widget_set_margin_top(row_box, 5);
gtk_widget_set_margin_bottom(row_box, 5);
// Создаем контейнер для информации о проекте
GtkWidget *info_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 3);
gtk_widget_set_hexpand(info_box, TRUE);
gtk_box_append(GTK_BOX(row_box), info_box);
// Добавляем название проекта
GtkWidget *name_label = gtk_label_new(project.name.c_str());
gtk_widget_add_css_class(name_label, "project-name");
gtk_label_set_xalign(GTK_LABEL(name_label), 0);
gtk_box_append(GTK_BOX(info_box), name_label);
// Добавляем путь проекта
GtkWidget *path_label = gtk_label_new(project.path.c_str());
gtk_widget_add_css_class(path_label, "project-path");
gtk_label_set_xalign(GTK_LABEL(path_label), 0);
gtk_widget_set_opacity(path_label, 0.7);
gtk_box_append(GTK_BOX(info_box), path_label);
// Добавляем кнопки действий
GtkWidget *button_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
gtk_box_append(GTK_BOX(row_box), button_box);
// Кнопка редактирования
GtkWidget *edit_button = gtk_button_new_from_icon_name("document-edit-symbolic");
gtk_widget_add_css_class(edit_button, "circular");
gtk_box_append(GTK_BOX(button_box), edit_button);
g_signal_connect(edit_button, "clicked", G_CALLBACK(on_edit_clicked), data);
// Кнопка удаления
GtkWidget *delete_button = gtk_button_new_from_icon_name("user-trash-symbolic");
gtk_widget_add_css_class(delete_button, "circular");
gtk_box_append(GTK_BOX(button_box), delete_button);
g_signal_connect(delete_button, "clicked", G_CALLBACK(on_delete_clicked), data);
gtk_list_box_append(data->project_list, row_box);
}
}
static gboolean destroy_on_key_pressed(GtkEventControllerKey *controller, guint keyval, guint keycode, GdkModifierType state, gpointer user_data)
{
if (keyval == GDK_KEY_Escape)
{
GtkWidget *widget = gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(controller));
GtkWindow *window = GTK_WINDOW(gtk_widget_get_ancestor(widget, GTK_TYPE_WINDOW));
gtk_window_destroy(window);
return TRUE;
}
return FALSE;
}
static void activate(GtkApplication *app, gpointer user_data)
{
auto *data = new AppData();
data->project_manager = new ProjectManager();
data->app = app;
// Загружаем CSS
GtkCssProvider *provider = gtk_css_provider_new();
const char *css_path = g_build_filename(g_get_home_dir(), ".config/project-launcher/style.css", NULL);
gtk_css_provider_load_from_path(provider, css_path);
gtk_style_context_add_provider_for_display(
gdk_display_get_default(),
GTK_STYLE_PROVIDER(provider),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
// Создаем главное окно через нашу функцию
GtkWidget *window = create_window("Менеджер проектов", nullptr);
gtk_window_set_application(GTK_WINDOW(window), app); // Важно для главного окна!
data->main_window = GTK_WINDOW(window);
gtk_window_set_default_size(GTK_WINDOW(window), 1200, 800);
// Создаем основной контейнер
GtkWidget *main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
gtk_widget_set_margin_start(main_box, 15);
gtk_widget_set_margin_end(main_box, 15);
gtk_widget_set_margin_top(main_box, 15);
gtk_widget_set_margin_bottom(main_box, 15);
gtk_window_set_child(GTK_WINDOW(window), main_box);
// Создаем верхнюю панель
GtkWidget *header_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
gtk_box_append(GTK_BOX(main_box), header_box);
// Добавляем поле поиска
GtkWidget *search_entry = gtk_search_entry_new();
gtk_widget_set_hexpand(search_entry, TRUE);
gtk_box_append(GTK_BOX(header_box), search_entry);
// Создаем кнопку добавления
GtkWidget *add_button = gtk_button_new_with_label("Добавить проект");
gtk_widget_add_css_class(add_button, "suggested-action");
gtk_box_append(GTK_BOX(header_box), add_button);
g_signal_connect(add_button, "clicked", G_CALLBACK(on_add_clicked), data);
// Создаем список проектов
GtkWidget *scrolled = gtk_scrolled_window_new();
gtk_widget_set_vexpand(scrolled, TRUE);
gtk_widget_add_css_class(scrolled, "projects-container");
gtk_box_append(GTK_BOX(main_box), scrolled);
GtkWidget *list_box = gtk_list_box_new();
data->project_list = GTK_LIST_BOX(list_box);
gtk_widget_add_css_class(list_box, "list-box");
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scrolled), list_box);
// Функция фильтрации
auto filter_func = [](GtkListBoxRow *row, gpointer data) -> gboolean
{
const char *search_text = gtk_editable_get_text(GTK_EDITABLE(data));
if (strlen(search_text) == 0)
return TRUE;
GtkWidget *box = gtk_list_box_row_get_child(row);
GtkWidget *info_box = gtk_widget_get_first_child(box);
GtkWidget *name_label = gtk_widget_get_first_child(info_box);
const char *name = gtk_label_get_text(GTK_LABEL(name_label));
// Преобразуем строки в нижний регистр для регистронезависимого поиска
char *name_lower = g_utf8_strdown(name, -1);
char *search_lower = g_utf8_strdown(search_text, -1);
gboolean found = strstr(name_lower, search_lower) != NULL;
g_free(name_lower);
g_free(search_lower);
return found;
};
// Устанавливаем функцию фильтрации
gtk_list_box_set_filter_func(data->project_list,
(GtkListBoxFilterFunc)filter_func, search_entry, NULL);
g_signal_connect(search_entry, "search-changed", G_CALLBACK(on_search_changed), data);
g_signal_connect(list_box, "row-activated", G_CALLBACK(on_project_activated), data);
// Обновляем список проектов
update_project_list(data);
// Устанавливаем фокус на первый элемент списка
GtkListBoxRow *first_row = gtk_list_box_get_row_at_index(data->project_list, 0);
if (first_row)
{
gtk_list_box_select_row(data->project_list, first_row);
gtk_widget_grab_focus(GTK_WIDGET(first_row));
}
gtk_window_present(GTK_WINDOW(window));
}
static GtkWidget *create_window(const char *title, GtkWindow *parent)
{
GtkWidget *window = gtk_window_new();
if (title)
{
gtk_window_set_title(GTK_WINDOW(window), title);
}
if (parent)
{
gtk_window_set_transient_for(GTK_WINDOW(window), parent);
gtk_window_set_modal(GTK_WINDOW(window), TRUE);
}
GtkEventController *controller = gtk_event_controller_key_new();
gtk_widget_add_controller(window, controller);
g_signal_connect(controller, "key-pressed", G_CALLBACK(destroy_on_key_pressed), NULL);
return window;
}
int main(int argc, char **argv)
{
GtkApplication *app = gtk_application_new("org.example.projectlauncher", G_APPLICATION_DEFAULT_FLAGS);
g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
int status = g_application_run(G_APPLICATION(app), argc, argv);
g_object_unref(app);
return status;
}

109
src/project_manager.cpp Normal file
View File

@ -0,0 +1,109 @@
#include "project_manager.hpp"
#include <fstream>
#include <iostream>
#include <cstdlib>
#include <filesystem>
ProjectManager::ProjectManager()
{
// Создаем директорию если её нет
std::string config_dir = std::string(getenv("HOME")) + "/.config/project-launcher";
std::filesystem::create_directories(config_dir);
loadFromFile();
}
ProjectManager::~ProjectManager()
{
saveToFile();
}
const std::vector<Project> &ProjectManager::getProjects() const
{
return projects;
}
void ProjectManager::addProject(const std::string &name, const std::string &path)
{
projects.push_back({name, path});
saveToFile();
}
void ProjectManager::deleteProject(size_t index)
{
if (index < projects.size())
{
projects.erase(projects.begin() + index);
saveToFile();
}
}
void ProjectManager::editProject(size_t index, const std::string &name, const std::string &path)
{
if (index < projects.size())
{
projects[index].name = name;
projects[index].path = path;
saveToFile();
}
}
bool ProjectManager::openProject(size_t index)
{
if (index < projects.size())
{
std::string command = "cursor " + projects[index].path + " &";
bool commandExecute = system(command.c_str());
std::cout << "Command executed: " << commandExecute << std::endl;
return commandExecute == 0;
}
return false;
}
void ProjectManager::saveToFile()
{
nlohmann::json j = nlohmann::json::array();
for (const auto &project : projects)
{
j.push_back({{"name", project.name},
{"path", project.path}});
}
std::ofstream file(CONFIG_FILE);
if (file.is_open())
{
file << j.dump(4);
file.close();
}
else
{
std::cerr << "Ошибка при сохранении файла!" << std::endl;
}
}
void ProjectManager::loadFromFile()
{
std::ifstream file(CONFIG_FILE);
if (file.is_open())
{
try
{
nlohmann::json j;
file >> j;
projects.clear();
for (const auto &item : j)
{
Project project;
project.name = item["name"];
project.path = item["path"];
projects.push_back(project);
}
}
catch (const std::exception &e)
{
std::cerr << "Ошибка при чтении файла: " << e.what() << std::endl;
}
file.close();
}
}

30
src/project_manager.hpp Normal file
View File

@ -0,0 +1,30 @@
#pragma once
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
struct Project
{
std::string name;
std::string path;
};
class ProjectManager
{
public:
ProjectManager();
~ProjectManager();
const std::vector<Project> &getProjects() const;
void addProject(const std::string &name, const std::string &path);
void deleteProject(size_t index);
void editProject(size_t index, const std::string &name, const std::string &path);
bool openProject(size_t index);
private:
std::vector<Project> projects;
const std::string CONFIG_FILE = std::string(getenv("HOME")) + "/.config/project-launcher/projects.json";
void saveToFile();
void loadFromFile();
};