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 @@
-[](https://opensource.org/licenses/MIT)
-[](https://nixos.wiki/wiki/Flakes)
+
+
+
-# 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.
+
+
+
+
+
+

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