From 6e1e41799e0794888db9e7c66dcf0c3237fd628f Mon Sep 17 00:00:00 2001 From: gofnnp Date: Mon, 22 Dec 2025 21:42:47 +0400 Subject: [PATCH] main initial git repo --- .gitignore | 2 + CMakeLists.txt | 23 +++ resources/style.css | 103 +++++++++++ src/main.cpp | 375 ++++++++++++++++++++++++++++++++++++++++ src/project_manager.cpp | 109 ++++++++++++ src/project_manager.hpp | 30 ++++ 6 files changed, 642 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 resources/style.css create mode 100644 src/main.cpp create mode 100644 src/project_manager.cpp create mode 100644 src/project_manager.hpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6a8bc10 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode +build \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d165843 --- /dev/null +++ b/CMakeLists.txt @@ -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 +) \ No newline at end of file diff --git a/resources/style.css b/resources/style.css new file mode 100644 index 0000000..0f9660b --- /dev/null +++ b/resources/style.css @@ -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; +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..f436ca8 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,375 @@ +#include +#include "project_manager.hpp" +#include + +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(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 *>(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(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; +} \ No newline at end of file diff --git a/src/project_manager.cpp b/src/project_manager.cpp new file mode 100644 index 0000000..1b22c05 --- /dev/null +++ b/src/project_manager.cpp @@ -0,0 +1,109 @@ +#include "project_manager.hpp" +#include +#include +#include +#include + +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 &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(); + } +} \ No newline at end of file diff --git a/src/project_manager.hpp b/src/project_manager.hpp new file mode 100644 index 0000000..66e98cf --- /dev/null +++ b/src/project_manager.hpp @@ -0,0 +1,30 @@ +#pragma once +#include +#include +#include + +struct Project +{ + std::string name; + std::string path; +}; + +class ProjectManager +{ +public: + ProjectManager(); + ~ProjectManager(); + + const std::vector &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 projects; + const std::string CONFIG_FILE = std::string(getenv("HOME")) + "/.config/project-launcher/projects.json"; + + void saveToFile(); + void loadFromFile(); +}; \ No newline at end of file