diff --git a/CMakeLists.txt b/CMakeLists.txt index b1835e3..ec76bb9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -85,6 +85,12 @@ add_subdirectory(src/core) add_subdirectory(src/cli) add_subdirectory(src/gui) +if(WIN32) + set(CLRSYNC_WINDOWS_RC ${CMAKE_SOURCE_DIR}/assets/icons/clrsync.rc) + target_sources(clrsync_cli PRIVATE ${CLRSYNC_WINDOWS_RC}) + target_sources(clrsync_gui PRIVATE ${CLRSYNC_WINDOWS_RC}) +endif() + include(Install) include(Packaging) diff --git a/README.md b/README.md index c4ed158..a677494 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,17 @@ -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Nix Flake](https://img.shields.io/badge/Nix-Flake-blue.svg)](https://nixos.wiki/wiki/Flakes) +

+ clrsync logo +

-# clrsync +

clrsync

-A theme management tool for synchronizing color schemes across multiple applications. clrsync allows to define color palettes once and apply them consistently to all configurable applications. +

+ A theme management tool for synchronizing color schemes across multiple applications. clrsync allows to define color palettes once and apply them consistently to all configurable applications. +

+ +

+ License: MIT + Nix Flake +

![Preview](assets/screenshot.png) diff --git a/assets/icons/clrsync-linux.png b/assets/icons/clrsync-linux.png new file mode 100644 index 0000000..977907b Binary files /dev/null and b/assets/icons/clrsync-linux.png differ diff --git a/assets/icons/clrsync.icns b/assets/icons/clrsync.icns new file mode 100644 index 0000000..27c47cd Binary files /dev/null and b/assets/icons/clrsync.icns differ diff --git a/assets/icons/clrsync.ico b/assets/icons/clrsync.ico new file mode 100644 index 0000000..d1dcab0 Binary files /dev/null and b/assets/icons/clrsync.ico differ diff --git a/assets/icons/clrsync.rc b/assets/icons/clrsync.rc new file mode 100644 index 0000000..58507c0 --- /dev/null +++ b/assets/icons/clrsync.rc @@ -0,0 +1,2 @@ +// Application icon (resource ID 1 = primary icon in Explorer and taskbar) +1 ICON "clrsync.ico" diff --git a/assets/icons/clrsync.svg b/assets/icons/clrsync.svg new file mode 100644 index 0000000..f53ddb7 --- /dev/null +++ b/assets/icons/clrsync.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/favicon.ico b/assets/icons/favicon.ico new file mode 100644 index 0000000..e6fd72b Binary files /dev/null and b/assets/icons/favicon.ico differ diff --git a/assets/icons/png/clrsync-1024.png b/assets/icons/png/clrsync-1024.png new file mode 100644 index 0000000..ae8afff Binary files /dev/null and b/assets/icons/png/clrsync-1024.png differ diff --git a/assets/icons/png/clrsync-128.png b/assets/icons/png/clrsync-128.png new file mode 100644 index 0000000..4031021 Binary files /dev/null and b/assets/icons/png/clrsync-128.png differ diff --git a/assets/icons/png/clrsync-16.png b/assets/icons/png/clrsync-16.png new file mode 100644 index 0000000..ed2520b Binary files /dev/null and b/assets/icons/png/clrsync-16.png differ diff --git a/assets/icons/png/clrsync-24.png b/assets/icons/png/clrsync-24.png new file mode 100644 index 0000000..d6cc1c1 Binary files /dev/null and b/assets/icons/png/clrsync-24.png differ diff --git a/assets/icons/png/clrsync-256.png b/assets/icons/png/clrsync-256.png new file mode 100644 index 0000000..b565d55 Binary files /dev/null and b/assets/icons/png/clrsync-256.png differ diff --git a/assets/icons/png/clrsync-32.png b/assets/icons/png/clrsync-32.png new file mode 100644 index 0000000..8afcda6 Binary files /dev/null and b/assets/icons/png/clrsync-32.png differ diff --git a/assets/icons/png/clrsync-48.png b/assets/icons/png/clrsync-48.png new file mode 100644 index 0000000..2efc757 Binary files /dev/null and b/assets/icons/png/clrsync-48.png differ diff --git a/assets/icons/png/clrsync-512.png b/assets/icons/png/clrsync-512.png new file mode 100644 index 0000000..977907b Binary files /dev/null and b/assets/icons/png/clrsync-512.png differ diff --git a/assets/icons/png/clrsync-64.png b/assets/icons/png/clrsync-64.png new file mode 100644 index 0000000..60bf265 Binary files /dev/null and b/assets/icons/png/clrsync-64.png differ diff --git a/assets/icons/web/logo-128.png b/assets/icons/web/logo-128.png new file mode 100644 index 0000000..4031021 Binary files /dev/null and b/assets/icons/web/logo-128.png differ diff --git a/assets/icons/web/logo-256.png b/assets/icons/web/logo-256.png new file mode 100644 index 0000000..b565d55 Binary files /dev/null and b/assets/icons/web/logo-256.png differ diff --git a/assets/icons/web/logo-512.png b/assets/icons/web/logo-512.png new file mode 100644 index 0000000..977907b Binary files /dev/null and b/assets/icons/web/logo-512.png differ diff --git a/src/cli/main.cpp b/src/cli/main.cpp index ce6d15f..a95b6f2 100644 --- a/src/cli/main.cpp +++ b/src/cli/main.cpp @@ -194,13 +194,23 @@ int main(int argc, char *argv[]) clrsync::core::palette pal; if (generator_name == "hellwal") { + clrsync::core::hellwal_generator gen; + if (!gen.supports_current_system()) + { + std::cerr << "Error: hellwal is not supported on " + << clrsync::core::generator::system_name( + clrsync::core::generator::current_system()) + << ". Supported systems: " << gen.supported_systems_description() + << std::endl; + return 1; + } + if (program.is_used("--generate-color")) { std::cerr << "Error: --generate-color is only supported with --generator matugen" << std::endl; return 1; } - clrsync::core::hellwal_generator gen; clrsync::core::hellwal_generator::options opts{}; if (program.is_used("--hellwal-neon")) @@ -246,6 +256,16 @@ int main(int argc, char *argv[]) else if (generator_name == "matugen") { clrsync::core::matugen_generator gen; + if (!gen.supports_current_system()) + { + std::cerr << "Error: matugen is not supported on " + << clrsync::core::generator::system_name( + clrsync::core::generator::current_system()) + << ". Supported systems: " << gen.supported_systems_description() + << std::endl; + return 1; + } + clrsync::core::matugen_generator::options opts{}; try diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 4707c3b..c545aae 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -4,8 +4,9 @@ set(CORE_SOURCES palette/matugen_generator.cpp io/toml_file.cpp config/config.cpp - common/utils.cpp - common/version.cpp + common/process.cpp + common/utils.cpp + common/version.cpp theme/theme_template.cpp ) diff --git a/src/core/common/process.cpp b/src/core/common/process.cpp new file mode 100644 index 0000000..c763bc7 --- /dev/null +++ b/src/core/common/process.cpp @@ -0,0 +1,246 @@ +#include "process.hpp" + +#include +#include +#include + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#include +#include +#endif + +namespace +{ +#ifdef _WIN32 +struct handle_guard +{ + HANDLE handle = nullptr; + + explicit handle_guard(HANDLE value) : handle(value) {} + + handle_guard(const handle_guard &) = delete; + handle_guard &operator=(const handle_guard &) = delete; + handle_guard(handle_guard &&other) noexcept : handle(other.handle) { other.handle = nullptr; } + handle_guard &operator=(handle_guard &&other) noexcept + { + if (this != &other) + { + reset(other.release()); + } + return *this; + } + + ~handle_guard() + { + if (handle) + CloseHandle(handle); + } + + HANDLE get() const { return handle; } + HANDLE release() + { + HANDLE value = handle; + handle = nullptr; + return value; + } + void reset(HANDLE value = nullptr) + { + if (handle) + CloseHandle(handle); + handle = value; + } +}; + +std::wstring utf8_to_wide(const std::string &value) +{ + if (value.empty()) + return {}; + + const int size = MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, nullptr, 0); + if (size <= 0) + return {}; + + std::wstring result(static_cast(size), L'\0'); + MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, result.data(), size); + result.pop_back(); + return result; +} + +std::wstring quote_windows_arg(const std::wstring &arg) +{ + if (arg.empty()) + return L"\"\""; + + if (arg.find_first_of(L" \t\n\v\"") == std::wstring::npos) + return arg; + + std::wstring quoted; + quoted.reserve(arg.size() + 2); + quoted.push_back(L'"'); + + size_t backslashes = 0; + for (wchar_t ch : arg) + { + if (ch == L'\\') + { + backslashes++; + continue; + } + + if (ch == L'"') + { + quoted.append(backslashes * 2 + 1, L'\\'); + quoted.push_back(L'"'); + backslashes = 0; + continue; + } + + if (backslashes > 0) + { + quoted.append(backslashes, L'\\'); + backslashes = 0; + } + + quoted.push_back(ch); + } + + if (backslashes > 0) + quoted.append(backslashes * 2, L'\\'); + + quoted.push_back(L'"'); + return quoted; +} + +std::wstring build_windows_command_line(const std::vector &args) +{ + std::wstring command_line; + for (size_t i = 0; i < args.size(); i++) + { + if (i > 0) + command_line.push_back(L' '); + command_line += quote_windows_arg(utf8_to_wide(args[i])); + } + return command_line; +} + +std::string run_windows_process(const std::vector &args) +{ + SECURITY_ATTRIBUTES sa{}; + sa.nLength = sizeof(sa); + sa.bInheritHandle = TRUE; + + HANDLE read_pipe_raw = nullptr; + HANDLE write_pipe_raw = nullptr; + if (!CreatePipe(&read_pipe_raw, &write_pipe_raw, &sa, 0)) + return {}; + + handle_guard read_pipe(read_pipe_raw); + handle_guard write_pipe(write_pipe_raw); + + if (!SetHandleInformation(read_pipe.get(), HANDLE_FLAG_INHERIT, 0)) + return {}; + + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + startup.dwFlags = STARTF_USESTDHANDLES; + startup.hStdOutput = write_pipe.get(); + startup.hStdError = write_pipe.get(); + startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + + PROCESS_INFORMATION process_info{}; + std::wstring command_line = build_windows_command_line(args); + std::vector mutable_command_line(command_line.begin(), command_line.end()); + mutable_command_line.push_back(L'\0'); + + if (!CreateProcessW(nullptr, mutable_command_line.data(), nullptr, nullptr, TRUE, + CREATE_NO_WINDOW, nullptr, nullptr, &startup, &process_info)) + { + return {}; + } + + handle_guard process_handle(process_info.hProcess); + handle_guard thread_handle(process_info.hThread); + write_pipe.reset(); + + std::string output; + std::array buffer{}; + DWORD bytes_read = 0; + while (ReadFile(read_pipe.get(), buffer.data(), static_cast(buffer.size()), + &bytes_read, nullptr) && + bytes_read > 0) + { + output.append(buffer.data(), buffer.data() + bytes_read); + } + + WaitForSingleObject(process_handle.get(), INFINITE); + return output; +} +#else +std::string run_posix_process(const std::vector &args) +{ + int pipe_fds[2]; + if (pipe(pipe_fds) != 0) + return {}; + + pid_t pid = fork(); + if (pid < 0) + { + close(pipe_fds[0]); + close(pipe_fds[1]); + return {}; + } + + if (pid == 0) + { + dup2(pipe_fds[1], STDOUT_FILENO); + dup2(pipe_fds[1], STDERR_FILENO); + + close(pipe_fds[0]); + close(pipe_fds[1]); + + std::vector argv; + argv.reserve(args.size() + 1); + for (const auto &arg : args) + argv.push_back(const_cast(arg.c_str())); + argv.push_back(nullptr); + + execvp(argv[0], argv.data()); + _exit(127); + } + + close(pipe_fds[1]); + + std::string output; + std::array buffer{}; + ssize_t bytes_read = 0; + while ((bytes_read = read(pipe_fds[0], buffer.data(), buffer.size())) > 0) + { + output.append(buffer.data(), static_cast(bytes_read)); + } + + close(pipe_fds[0]); + int status = 0; + (void)waitpid(pid, &status, 0); + return output; +} +#endif +} // namespace + +namespace clrsync::core +{ +std::string run_process_capture_output(const std::vector &args) +{ + if (args.empty()) + return {}; + +#ifdef _WIN32 + return run_windows_process(args); +#else + return run_posix_process(args); +#endif +} +} // namespace clrsync::core diff --git a/src/core/common/process.hpp b/src/core/common/process.hpp new file mode 100644 index 0000000..4230b3e --- /dev/null +++ b/src/core/common/process.hpp @@ -0,0 +1,12 @@ +#ifndef CLRSYNC_CORE_COMMON_PROCESS_HPP +#define CLRSYNC_CORE_COMMON_PROCESS_HPP + +#include +#include + +namespace clrsync::core +{ +std::string run_process_capture_output(const std::vector &args); +} // namespace clrsync::core + +#endif // CLRSYNC_CORE_COMMON_PROCESS_HPP diff --git a/src/core/common/version.hpp b/src/core/common/version.hpp index 267e84c..f875967 100644 --- a/src/core/common/version.hpp +++ b/src/core/common/version.hpp @@ -6,7 +6,7 @@ namespace clrsync::core { -const std::string GIT_SEMVER = "1.1.2+git.g9d4cb72"; +const std::string GIT_SEMVER = "1.2.1+git.g7280102"; const std::string version_string(); } // namespace clrsync::core diff --git a/src/core/palette/generator.hpp b/src/core/palette/generator.hpp index 1fd9cb3..3aa2e71 100644 --- a/src/core/palette/generator.hpp +++ b/src/core/palette/generator.hpp @@ -1,7 +1,11 @@ #ifndef CLRSYNC_CORE_PALETTE_GENERATOR_HPP #define CLRSYNC_CORE_PALETTE_GENERATOR_HPP +#include +#include +#include #include +#include #include "core/palette/palette.hpp" namespace clrsync::core @@ -9,10 +13,91 @@ namespace clrsync::core class generator { public: - generator() = default; + enum class system + { + windows, + linux_os, + macos, + unknown + }; + + explicit generator(std::initializer_list supported_systems) + : m_supported_systems(supported_systems) + { + } + virtual ~generator() = default; virtual palette generate_from_image(const std::string &image_path) = 0; + + [[nodiscard]] bool supports_current_system() const + { + return supports_system(current_system()); + } + + [[nodiscard]] bool supports_system(system value) const + { + return std::find(m_supported_systems.begin(), m_supported_systems.end(), value) != + m_supported_systems.end(); + } + + [[nodiscard]] const std::vector &supported_systems() const noexcept + { + return m_supported_systems; + } + + [[nodiscard]] std::string supported_systems_description() const + { + std::string out; + for (const auto system : m_supported_systems) + { + if (!out.empty()) + out += ", "; + out += system_name(system); + } + return out; + } + + static system current_system() noexcept + { +#ifdef _WIN32 + return system::windows; +#elif defined(__APPLE__) + return system::macos; +#elif defined(__linux__) + return system::linux_os; +#else + return system::unknown; +#endif + } + + static std::string system_name(system value) + { + switch (value) + { + case system::windows: + return "Windows"; + case system::linux_os: + return "Linux"; + case system::macos: + return "macOS"; + default: + return "unknown"; + } + } + + void ensure_supported(const std::string &generator_name) const + { + if (supports_current_system()) + return; + + throw std::runtime_error(generator_name + " is not supported on " + + system_name(current_system()) + + ". Supported systems: " + supported_systems_description()); + } + + protected: + std::vector m_supported_systems; }; } // namespace clrsync::core diff --git a/src/core/palette/hellwal_generator.cpp b/src/core/palette/hellwal_generator.cpp index 94fd902..682a313 100644 --- a/src/core/palette/hellwal_generator.cpp +++ b/src/core/palette/hellwal_generator.cpp @@ -1,40 +1,18 @@ #include "hellwal_generator.hpp" +#include "core/common/process.hpp" #include "core/palette/color.hpp" #include "core/palette/json_utils.hpp" -#include #include #include -#include #include #include -#ifdef _WIN32 -#define popen _popen -#define pclose _pclose -#endif - namespace clrsync::core { using json = json_utils::json; -static std::string run_command_capture_output(const std::string &cmd) -{ - std::array buffer; - std::string result; - FILE *pipe = popen(cmd.c_str(), "r"); - if (!pipe) - return {}; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != nullptr) - { - result += buffer.data(); - } - int rc = pclose(pipe); - (void)rc; - return result; -} - static void collect_palette_colors(const json &node, palette &pal) { if (node.is_object()) @@ -91,31 +69,42 @@ palette hellwal_generator::generate_from_image(const std::string &image_path) palette hellwal_generator::generate_from_image(const std::string &image_path, const options &opts) { + ensure_supported("hellwal"); + palette pal; std::filesystem::path p(image_path); pal.set_name("hellwal:" + p.filename().string()); pal.set_file_path(image_path); - std::string cmd = "hellwal -i '" + image_path + "' --json"; + std::vector args = {"hellwal", "-i", image_path, "--json"}; if (opts.neon) - cmd += " --neon-mode"; + args.push_back("--neon-mode"); if (opts.dark) - cmd += " --dark"; + args.push_back("--dark"); if (opts.light) - cmd += " --light"; + args.push_back("--light"); if (opts.color) - cmd += " --color"; + args.push_back("--color"); if (opts.dark_offset > 0.0f) - cmd += " --dark-offset " + std::to_string(opts.dark_offset); + { + args.push_back("--dark-offset"); + args.push_back(std::to_string(opts.dark_offset)); + } if (opts.bright_offset > 0.0f) - cmd += " --bright-offset " + std::to_string(opts.bright_offset); + { + args.push_back("--bright-offset"); + args.push_back(std::to_string(opts.bright_offset)); + } if (opts.invert) - cmd += " --invert"; + args.push_back("--invert"); if (opts.gray_scale > 0.0f) - cmd += " --gray-scale " + std::to_string(opts.gray_scale); + { + args.push_back("--gray-scale"); + args.push_back(std::to_string(opts.gray_scale)); + } - std::string out = run_command_capture_output(cmd); + std::string out = run_process_capture_output(args); if (out.empty()) return {}; diff --git a/src/core/palette/hellwal_generator.hpp b/src/core/palette/hellwal_generator.hpp index 469915a..749335d 100644 --- a/src/core/palette/hellwal_generator.hpp +++ b/src/core/palette/hellwal_generator.hpp @@ -9,7 +9,10 @@ namespace clrsync::core class hellwal_generator : public generator { public: - hellwal_generator() = default; + hellwal_generator() + : generator({generator::system::linux_os, generator::system::macos}) + { + } ~hellwal_generator() override = default; struct options diff --git a/src/core/palette/matugen_generator.cpp b/src/core/palette/matugen_generator.cpp index 3a7fe87..567589f 100644 --- a/src/core/palette/matugen_generator.cpp +++ b/src/core/palette/matugen_generator.cpp @@ -1,39 +1,17 @@ #include "matugen_generator.hpp" +#include "core/common/process.hpp" #include "core/palette/color.hpp" #include "core/palette/json_utils.hpp" -#include -#include #include #include #include -#ifdef _WIN32 -#define popen _popen -#define pclose _pclose -#endif - namespace clrsync::core { using json = json_utils::json; -static std::string run_command_capture_output(const std::string &cmd) -{ - std::array buffer; - std::string result; - FILE *pipe = popen(cmd.c_str(), "r"); - if (!pipe) - return {}; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != nullptr) - { - result += buffer.data(); - } - int rc = pclose(pipe); - (void)rc; - return result; -} - static palette parse_matugen_output(const std::string &out, const matugen_generator::options &opts, const std::string &pal_name, const std::string &file_path) { @@ -203,23 +181,36 @@ palette matugen_generator::generate_from_image(const std::string &image_path) palette matugen_generator::generate_from_image(const std::string &image_path, const options &opts) { + ensure_supported("matugen"); + std::filesystem::path p(image_path); - std::string cmd = "matugen image '" + image_path + "'"; + std::vector args = {"matugen", "image", image_path}; if (!opts.type.empty()) - cmd += " --type '" + opts.type + "'"; + { + args.push_back("--type"); + args.push_back(opts.type); + } if (!opts.mode.empty()) - cmd += " --mode " + opts.mode; + { + args.push_back("--mode"); + args.push_back(opts.mode); + } if (opts.contrast != 0.0f) - cmd += " --contrast " + std::to_string(opts.contrast); - cmd += " --json hex --dry-run"; + { + args.push_back("--contrast"); + args.push_back(std::to_string(opts.contrast)); + } + args.push_back("--json"); + args.push_back("hex"); + args.push_back("--dry-run"); if (opts.source_color_index >= 0 && opts.source_color_index <= 3) { - cmd += " --source-color-index "; - cmd += std::to_string(opts.source_color_index); + args.push_back("--source-color-index"); + args.push_back(std::to_string(opts.source_color_index)); } - std::string out = run_command_capture_output(cmd); + std::string out = run_process_capture_output(args); if (out.empty()) return {}; return parse_matugen_output(out, opts, std::string("matugen:") + p.filename().string(), @@ -234,20 +225,33 @@ palette matugen_generator::generate_from_color(const std::string &color_hex) palette matugen_generator::generate_from_color(const std::string &color_hex, const options &opts) { + ensure_supported("matugen"); + std::string c = color_hex; if (!c.empty() && c[0] == '#') c = c.substr(1); - std::string cmd = "matugen color hex '" + c + "'"; + std::vector args = {"matugen", "color", "hex", c}; if (!opts.type.empty()) - cmd += " --type '" + opts.type + "'"; + { + args.push_back("--type"); + args.push_back(opts.type); + } if (!opts.mode.empty()) - cmd += " --mode " + opts.mode; + { + args.push_back("--mode"); + args.push_back(opts.mode); + } if (opts.contrast != 0.0f) - cmd += " --contrast " + std::to_string(opts.contrast); - cmd += " --json hex --dry-run"; + { + args.push_back("--contrast"); + args.push_back(std::to_string(opts.contrast)); + } + args.push_back("--json"); + args.push_back("hex"); + args.push_back("--dry-run"); - std::string out = run_command_capture_output(cmd); + std::string out = run_process_capture_output(args); if (out.empty()) return {}; diff --git a/src/core/palette/matugen_generator.hpp b/src/core/palette/matugen_generator.hpp index 58269d8..25de48e 100644 --- a/src/core/palette/matugen_generator.hpp +++ b/src/core/palette/matugen_generator.hpp @@ -9,7 +9,11 @@ namespace clrsync::core class matugen_generator : public generator { public: - matugen_generator() = default; + matugen_generator() + : generator({generator::system::windows, generator::system::linux_os, + generator::system::macos}) + { + } ~matugen_generator() override = default; struct options diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 6bc9020..d18c40d 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -31,6 +31,9 @@ set(GUI_SOURCES platform/macos/font_loader_macos.cpp platform/linux/file_browser_linux.cpp platform/windows/file_browser_windows.cpp + platform/windows/window_icon_windows.cpp + platform/linux/window_icon_linux.cpp + platform/macos/window_icon_macos.cpp ${CMAKE_SOURCE_DIR}/lib/color_text_edit/TextEditor.cpp backend/glfw_opengl.cpp ui_manager.cpp diff --git a/src/gui/backend/glfw_opengl.cpp b/src/gui/backend/glfw_opengl.cpp index 7c5d54b..d6a435f 100644 --- a/src/gui/backend/glfw_opengl.cpp +++ b/src/gui/backend/glfw_opengl.cpp @@ -1,4 +1,5 @@ #include "gui/backend/glfw_opengl.hpp" +#include "gui/platform/window_icon.hpp" #include #include #include @@ -54,6 +55,7 @@ bool glfw_opengl_backend::initialize(const window_config &config) glfwMakeContextCurrent(m_window); glfwSwapInterval(1); glfwSetWindowFocusCallback(m_window, focus_callback); + platform::set_window_icon(m_window); return true; } diff --git a/src/gui/platform/file_browser.hpp b/src/gui/platform/file_browser.hpp index dcab9a3..c3a14ec 100644 --- a/src/gui/platform/file_browser.hpp +++ b/src/gui/platform/file_browser.hpp @@ -16,6 +16,12 @@ std::string save_file_dialog(const std::string &title = "Save File", std::string select_folder_dialog(const std::string &title = "Select Folder", const std::string &initial_path = ""); + +inline const std::vector &image_file_filters() +{ + static const std::vector filters = {"png", "jpg", "jpeg", "bmp"}; + return filters; +} } // namespace file_dialogs -#endif // CLRSYNC_GUI_FILE_BROWSER_HPP \ No newline at end of file +#endif // CLRSYNC_GUI_FILE_BROWSER_HPP diff --git a/src/gui/platform/linux/window_icon_linux.cpp b/src/gui/platform/linux/window_icon_linux.cpp new file mode 100644 index 0000000..9b29b42 --- /dev/null +++ b/src/gui/platform/linux/window_icon_linux.cpp @@ -0,0 +1,16 @@ +#ifdef __linux__ + +#include + +#include "gui/platform/window_icon.hpp" + +namespace clrsync::gui::platform +{ + +void set_window_icon(GLFWwindow *) +{ +} + +} // namespace clrsync::gui::platform + +#endif // __linux__ diff --git a/src/gui/platform/macos/window_icon_macos.cpp b/src/gui/platform/macos/window_icon_macos.cpp new file mode 100644 index 0000000..9d14847 --- /dev/null +++ b/src/gui/platform/macos/window_icon_macos.cpp @@ -0,0 +1,16 @@ +#ifdef __APPLE__ + +#include + +#include "gui/platform/window_icon.hpp" + +namespace clrsync::gui::platform +{ + +void set_window_icon(GLFWwindow *) +{ +} + +} // namespace clrsync::gui::platform + +#endif // __APPLE__ diff --git a/src/gui/platform/window_icon.hpp b/src/gui/platform/window_icon.hpp new file mode 100644 index 0000000..06f59a6 --- /dev/null +++ b/src/gui/platform/window_icon.hpp @@ -0,0 +1,13 @@ +#ifndef CLRSYNC_GUI_PLATFORM_WINDOW_ICON_HPP +#define CLRSYNC_GUI_PLATFORM_WINDOW_ICON_HPP + +struct GLFWwindow; + +namespace clrsync::gui::platform +{ + +void set_window_icon(GLFWwindow *window); + +} // namespace clrsync::gui::platform + +#endif // CLRSYNC_GUI_PLATFORM_WINDOW_ICON_HPP diff --git a/src/gui/platform/windows/file_browser_windows.cpp b/src/gui/platform/windows/file_browser_windows.cpp index b136554..2d6deec 100644 --- a/src/gui/platform/windows/file_browser_windows.cpp +++ b/src/gui/platform/windows/file_browser_windows.cpp @@ -10,6 +10,64 @@ #include "gui/platform/file_browser.hpp" #include +#include + +namespace +{ +std::string normalize_filter_pattern(const std::string &filter) +{ + if (filter.empty()) + return {}; + + if (filter.find_first_of("*?;") != std::string::npos) + return filter; + + if (filter[0] == '.') + return "*" + filter; + + return "*." + filter; +} + +std::string build_filter_buffer(const std::vector &filters) +{ + std::string buffer; + + auto append_entry = [&](const std::string &label, const std::string &pattern) { + buffer.append(label); + buffer.push_back('\0'); + buffer.append(pattern); + buffer.push_back('\0'); + }; + + if (filters.empty()) + { + append_entry("All Files (*.*)", "*.*"); + buffer.push_back('\0'); + return buffer; + } + + std::ostringstream patterns; + bool first = true; + for (const auto &filter : filters) + { + const std::string pattern = normalize_filter_pattern(filter); + if (pattern.empty()) + continue; + + if (!first) + patterns << ';'; + patterns << pattern; + first = false; + } + + const std::string pattern_list = patterns.str(); + if (!pattern_list.empty()) + append_entry("Image Files", pattern_list); + append_entry("All Files (*.*)", "*.*"); + buffer.push_back('\0'); + return buffer; +} +} // namespace namespace file_dialogs { @@ -20,7 +78,7 @@ std::string open_file_dialog(const std::string &title, const std::string &initia OPENFILENAMEA ofn; char file[MAX_PATH] = ""; - std::string filter_str = "All Files (*.*)\0*.*\0"; + std::string filter_str = build_filter_buffer(filters); ZeroMemory(&ofn, sizeof(ofn)); ofn.lStructSize = sizeof(ofn); @@ -61,7 +119,7 @@ std::string save_file_dialog(const std::string &title, const std::string &initia OPENFILENAMEA ofn; char file[MAX_PATH] = ""; - std::string filter_str = "All Files\0*.*\0\0"; + std::string filter_str = build_filter_buffer(filters); ZeroMemory(&ofn, sizeof(ofn)); ofn.lStructSize = sizeof(ofn); @@ -164,4 +222,4 @@ std::string select_folder_dialog(const std::string &title, const std::string &in } // namespace file_dialogs -#endif \ No newline at end of file +#endif diff --git a/src/gui/platform/windows/window_icon_windows.cpp b/src/gui/platform/windows/window_icon_windows.cpp new file mode 100644 index 0000000..a97e9e5 --- /dev/null +++ b/src/gui/platform/windows/window_icon_windows.cpp @@ -0,0 +1,58 @@ +#ifdef _WIN32 + +#define GLFW_EXPOSE_NATIVE_WIN32 +#include +#include + +#include "gui/platform/window_icon.hpp" + +#define WIN32_LEAN_AND_MEAN +#include + +namespace +{ + +constexpr int APP_ICON_RESOURCE_ID = 1; + +HICON load_app_icon(int width, int height) +{ + HINSTANCE instance = GetModuleHandleW(nullptr); + HICON icon = static_cast(LoadImageW(instance, MAKEINTRESOURCEW(APP_ICON_RESOURCE_ID), + IMAGE_ICON, width, height, LR_DEFAULTCOLOR)); + if (!icon && (width != 0 || height != 0)) + { + icon = static_cast(LoadImageW(instance, MAKEINTRESOURCEW(APP_ICON_RESOURCE_ID), + IMAGE_ICON, 0, 0, LR_DEFAULTSIZE)); + } + return icon; +} + +void apply_icon(HWND hwnd, UINT icon_type, HICON icon) +{ + if (!icon) + return; + HICON previous = reinterpret_cast(SendMessageW(hwnd, WM_SETICON, icon_type, + reinterpret_cast(icon))); + if (previous) + DestroyIcon(previous); +} + +} // namespace + +namespace clrsync::gui::platform +{ + +void set_window_icon(GLFWwindow *window) +{ + HWND hwnd = glfwGetWin32Window(window); + if (!hwnd) + return; + apply_icon(hwnd, ICON_SMALL, + load_app_icon(GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON))); + apply_icon(hwnd, ICON_BIG, + load_app_icon(GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON))); +} + +} // namespace clrsync::gui::platform + +#endif // _WIN32 diff --git a/src/gui/views/color_scheme_editor.cpp b/src/gui/views/color_scheme_editor.cpp index d54f5ea..55848ec 100644 --- a/src/gui/views/color_scheme_editor.cpp +++ b/src/gui/views/color_scheme_editor.cpp @@ -14,6 +14,37 @@ #include #include +void color_scheme_editor::refresh_available_generators() +{ + m_available_generators.clear(); + m_generator_labels.clear(); + + clrsync::core::hellwal_generator hellwal_gen; + if (hellwal_gen.supports_current_system()) + { + m_available_generators.push_back(generator_kind::hellwal); + m_generator_labels.push_back("hellwal"); + } + + clrsync::core::matugen_generator matugen_gen; + if (matugen_gen.supports_current_system()) + { + m_available_generators.push_back(generator_kind::matugen); + m_generator_labels.push_back("matugen"); + } + + if (m_generator_idx < 0 || m_generator_idx >= static_cast(m_available_generators.size())) + m_generator_idx = 0; +} + +std::optional color_scheme_editor::selected_generator_kind() const +{ + if (m_generator_idx < 0 || m_generator_idx >= static_cast(m_available_generators.size())) + return std::nullopt; + + return m_available_generators[m_generator_idx]; +} + color_scheme_editor::color_scheme_editor() { const auto ¤t = m_controller.current_palette(); @@ -28,6 +59,7 @@ color_scheme_editor::color_scheme_editor() std::cout << "WARNING: No palette loaded, skipping theme application\n"; } + refresh_available_generators(); setup_widgets(); } @@ -122,12 +154,27 @@ void color_scheme_editor::render_controls() if (ImGui::BeginPopupModal("Generate Palette", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::Text("Generator:"); - const char *generators[] = {"hellwal", "matugen"}; - ImGui::SameLine(); - ImGui::SetNextItemWidth(160.0f); - ImGui::Combo("##gen_select", &m_generator_idx, generators, IM_ARRAYSIZE(generators)); + std::vector generator_items; + generator_items.reserve(m_generator_labels.size()); + for (const auto &label : m_generator_labels) + generator_items.push_back(label.c_str()); - if (m_generator_idx == 0) // hellwal + if (!generator_items.empty()) + { + ImGui::SameLine(); + ImGui::SetNextItemWidth(160.0f); + ImGui::Combo("##gen_select", &m_generator_idx, generator_items.data(), + static_cast(generator_items.size())); + } + else + { + ImGui::SameLine(); + ImGui::TextDisabled("No supported generators"); + } + + const auto selected_kind = selected_generator_kind(); + + if (selected_kind && *selected_kind == generator_kind::hellwal) { ImGui::Separator(); ImGui::Text("hellwal options"); @@ -149,8 +196,9 @@ void color_scheme_editor::render_controls() ImGui::SameLine(); if (ImGui::Button("Browse##gen_image")) { - std::string res = file_dialogs::open_file_dialog("Select Image", m_gen_image_path, - {"png", "jpg", "jpeg", "bmp"}); + std::string res = + file_dialogs::open_file_dialog("Select Image", m_gen_image_path, + file_dialogs::image_file_filters()); if (!res.empty()) m_gen_image_path = res; } @@ -170,7 +218,7 @@ void color_scheme_editor::render_controls() ImGui::SliderFloat("Gray scale", &m_gen_gray_scale, 0.0f, 1.0f); } - if (m_generator_idx == 1) // matugen + if (selected_kind && *selected_kind == generator_kind::matugen) { ImGui::Separator(); ImGui::Text("matugen options"); @@ -191,8 +239,9 @@ void color_scheme_editor::render_controls() ImGui::SameLine(); if (ImGui::Button("Browse##gen_image")) { - std::string res = file_dialogs::open_file_dialog("Select Image", m_gen_image_path, - {"png", "jpg", "jpeg", "bmp"}); + std::string res = + file_dialogs::open_file_dialog("Select Image", m_gen_image_path, + file_dialogs::image_file_filters()); if (!res.empty()) m_gen_image_path = res; } @@ -245,13 +294,27 @@ void color_scheme_editor::render_controls() } ImGui::Separator(); + const bool can_generate = selected_kind.has_value(); + if (!can_generate) + ImGui::BeginDisabled(); if (ImGui::Button("Generate", ImVec2(120, 0))) { try { - if (m_generator_idx == 0) + if (selected_kind && *selected_kind == generator_kind::hellwal) { clrsync::core::hellwal_generator gen; + if (!gen.supports_current_system()) + { + std::cerr << "Generation failed: hellwal is not supported on " + << clrsync::core::generator::system_name( + clrsync::core::generator::current_system()) + << std::endl; + ImGui::CloseCurrentPopup(); + if (!can_generate) + ImGui::EndDisabled(); + return; + } clrsync::core::hellwal_generator::options opts; opts.neon = m_gen_neon; opts.dark = m_gen_dark; @@ -265,8 +328,9 @@ void color_scheme_editor::render_controls() auto image_path = m_gen_image_path; if (image_path.empty()) { - image_path = file_dialogs::open_file_dialog("Select Image", "", - {"png", "jpg", "jpeg", "bmp"}); + image_path = + file_dialogs::open_file_dialog("Select Image", "", + file_dialogs::image_file_filters()); } auto pal = gen.generate_from_image(image_path, opts); @@ -279,9 +343,20 @@ void color_scheme_editor::render_controls() m_controller.select_palette(pal.name()); apply_themes(); } - else if (m_generator_idx == 1) + else if (selected_kind && *selected_kind == generator_kind::matugen) { clrsync::core::matugen_generator gen; + if (!gen.supports_current_system()) + { + std::cerr << "Generation failed: matugen is not supported on " + << clrsync::core::generator::system_name( + clrsync::core::generator::current_system()) + << std::endl; + ImGui::CloseCurrentPopup(); + if (!can_generate) + ImGui::EndDisabled(); + return; + } clrsync::core::matugen_generator::options opts; opts.mode = m_matugen_mode; opts.type = m_matugen_type; @@ -308,7 +383,7 @@ void color_scheme_editor::render_controls() if (image_path.empty()) { image_path = file_dialogs::open_file_dialog( - "Select Image", "", {"png", "jpg", "jpeg", "bmp"}); + "Select Image", "", file_dialogs::image_file_filters()); } pal = gen.generate_from_image(image_path, opts); if (pal.name().empty()) @@ -331,8 +406,14 @@ void color_scheme_editor::render_controls() { std::cerr << "Generation failed: " << e.what() << std::endl; } + if (!can_generate) + ImGui::EndDisabled(); ImGui::CloseCurrentPopup(); } + else if (!can_generate) + { + ImGui::EndDisabled(); + } ImGui::SameLine(); if (ImGui::Button("Cancel", ImVec2(120, 0))) { @@ -374,6 +455,14 @@ void color_scheme_editor::setup_widgets() try { clrsync::core::hellwal_generator gen; + if (!gen.supports_current_system()) + { + std::cerr << "Failed to generate palette: hellwal is not supported on " + << clrsync::core::generator::system_name( + clrsync::core::generator::current_system()) + << std::endl; + return; + } auto pal = gen.generate_from_image(image_path); if (pal.name().empty()) { @@ -392,7 +481,7 @@ void color_scheme_editor::setup_widgets() m_generate_dialog.set_path_browse_callback( [this](const std::string ¤t_path) -> std::string { return file_dialogs::open_file_dialog("Select Image", current_path, - {"png", "jpg", "jpeg", "bmp"}); + file_dialogs::image_file_filters()); }); m_action_buttons.add_button({" Save ", "Save current palette to file", diff --git a/src/gui/views/color_scheme_editor.hpp b/src/gui/views/color_scheme_editor.hpp index 12e09ea..b1979c2 100644 --- a/src/gui/views/color_scheme_editor.hpp +++ b/src/gui/views/color_scheme_editor.hpp @@ -7,6 +7,9 @@ #include "gui/widgets/action_buttons.hpp" #include "gui/widgets/input_dialog.hpp" #include "gui/widgets/palette_selector.hpp" +#include +#include +#include class template_editor; class settings_window; @@ -14,6 +17,12 @@ class settings_window; class color_scheme_editor { public: + enum class generator_kind + { + hellwal, + matugen + }; + color_scheme_editor(); void render_controls_and_colors(); @@ -36,6 +45,8 @@ class color_scheme_editor void apply_themes(); void notify_palette_changed(); void setup_widgets(); + void refresh_available_generators(); + [[nodiscard]] std::optional selected_generator_kind() const; palette_controller m_controller; color_table_renderer m_color_table; @@ -68,7 +79,9 @@ class color_scheme_editor bool m_matugen_use_color{false}; float m_matugen_color_vec[3]{1.0f, 0.0f, 0.0f}; std::string m_matugen_color_hex{"FF0000"}; + std::vector m_available_generators; + std::vector m_generator_labels; clrsync::gui::widgets::action_buttons m_action_buttons; }; -#endif // CLRSYNC_GUI_COLOR_SCHEME_EDITOR_HPP \ No newline at end of file +#endif // CLRSYNC_GUI_COLOR_SCHEME_EDITOR_HPP