79 Commits

Author SHA1 Message Date
5b641cdd02 fix: typo in font_loader.windows 2025-12-19 08:21:37 +03:00
dff3e916fe bump version 2025-12-19 00:23:02 +03:00
2d653834a5 fix(nixos): write to config.temp.toml if config.toml is unwriteable 2025-12-19 00:19:26 +03:00
9be0a159ea fixed limux build 2025-12-18 23:53:06 +03:00
0288773ccb revert removed deps 2025-12-18 23:33:04 +03:00
c68ca3dabe wip: moving backend-related stuff out of the app logic 2025-12-18 16:45:49 +03:00
292a748ac4 chore: minor adjustments 2025-12-18 14:57:15 +03:00
7641846600 chore: structured src/gui, run clang-format 2025-12-18 13:23:50 +03:00
613c2c80f5 build: fix msvc builds 2025-12-18 11:51:28 +03:00
1a1747a472 chore: got build working on mac (kind of) 2025-12-18 01:29:32 +03:00
57c3c55a94 bump version 2025-12-18 00:45:10 +03:00
d4ff415f45 fix: gtk dialogs freezing 2025-12-18 00:42:47 +03:00
4c0502d8ee fix missing XDG_DATA_DIRS on NixOS 2025-12-18 00:11:55 +03:00
0acb36445f adjusted default theme 2025-12-17 16:13:14 +03:00
b08ba4d754 fix: coloring 2025-12-17 15:49:48 +03:00
231e9f0176 updated preview 2025-12-17 14:46:32 +03:00
b98761a172 chore: fixed build deps 2025-12-17 13:42:48 +03:00
92b06a9e0c bump version 2025-12-17 13:33:25 +03:00
019b0db522 build: added gtk dep 2025-12-17 13:32:26 +03:00
58eff4d97e chore: cleanup the ui 2025-12-17 13:14:46 +03:00
b4ca5e1912 feat: autocomplete in template editor 2025-12-17 11:45:46 +03:00
899a5d50c4 removed eyedropper for now 2025-12-17 10:45:35 +03:00
2813a8bd05 feat: added eyedropper 2025-12-17 10:21:33 +03:00
e6bac8e220 feat: init palettes with default colorscheme to avoid messed up UI 2025-12-17 09:50:14 +03:00
5bb8a687ea fix: added delete confirmation dialog in color_scheme_editor 2025-12-17 08:37:15 +03:00
10516212bf fix (fonts): load exact match 2025-12-17 08:32:56 +03:00
89888adf8d chore: add doc link 2025-12-17 03:11:15 +03:00
c58ff17289 feat: font selector 2025-12-17 03:06:24 +03:00
ef0854aa39 fix: normalize paths 2025-12-17 02:58:23 +03:00
1c2486d476 feat: allow to remove templates 2025-12-17 02:49:52 +03:00
d4c563f585 refactor: error handling with err objects 2025-12-17 02:25:21 +03:00
f7c290110e chore: split color_scheme_editor 2025-12-17 01:41:44 +03:00
659c5f28e5 versioning 2025-12-16 00:37:18 +03:00
cd817446b0 versioning (WIP) 2025-12-15 23:46:47 +03:00
a5d6503305 set version in flake 2025-12-15 21:10:23 +03:00
8a2b224fd3 set git version 2025-12-15 20:55:54 +03:00
4b4af0f8fe updated test flake workflow 2025-12-15 13:23:31 +03:00
d40b436461 updated readme 2025-12-15 13:22:29 +03:00
8d73df8fb8 publish releases 2025-12-15 13:08:53 +03:00
c4bab31e3b added write permission for release step 2025-12-15 12:30:24 +03:00
8e65c52adc use ncipollo/release-action 2025-12-15 12:20:15 +03:00
164e6f9ac0 ci: do not use matrix 2025-12-15 12:10:49 +03:00
d951f8d9c8 ci: merged windows and linux builds 2025-12-15 12:03:13 +03:00
794193209b typo 2025-12-15 11:52:43 +03:00
2a10aa0226 ci: removed install cmake step for windows 2025-12-15 11:49:17 +03:00
8caddbbb80 ci: removed extra build for windows 2025-12-15 11:43:52 +03:00
c1474ccf0c ci: build only NSIS for windows 2025-12-15 11:39:44 +03:00
db4cc383d4 ci: test windows installer 2025-12-15 11:31:30 +03:00
52a4b096a5 updated readme 2025-12-15 11:11:22 +03:00
1e2c7faa38 cleaned up module and added package with overlay 2025-12-15 11:09:14 +03:00
cc4d8f9dbd updated readme 2025-12-15 01:19:07 +03:00
ad92d366b2 try to set default package 2025-12-15 00:24:26 +03:00
e44d441453 Merge branch 'master' of github.com:obsqrbtz/clrsync 2025-12-14 23:47:09 +03:00
bb1c14d566 fixed typo 2025-12-14 23:47:04 +03:00
2714ae51b7 Update README.md 2025-12-13 03:11:18 +03:00
881bc6e739 Update README.md 2025-12-13 03:03:18 +03:00
65e54f9c0b docs: added nixos instructions 2025-12-13 02:43:08 +03:00
2c452cb395 added home manager module 2025-12-13 02:25:14 +03:00
2a81fa7b1b updated flake 2025-12-12 14:06:33 +03:00
cf8c93e31b ci: set latest nix action ver 2025-12-09 16:36:01 +03:00
8770dbcef8 typo 2025-12-09 16:33:50 +03:00
236f948fcf ci: added flake test 2025-12-09 16:31:22 +03:00
3350c41ccc use sekf as source 2025-12-09 15:56:42 +03:00
44a34eb216 fix: do not copy whole dirs (doesnt work on nix-store) 2025-12-09 15:46:17 +03:00
7535bb51ce split cmakelists 2025-12-09 15:03:46 +03:00
4c135edc95 build: link freerype with imgui isstead of clrsync_gui 2025-12-09 14:47:31 +03:00
813396920c ci: add libxkbcommon-dev for ubuntu 2025-12-09 14:22:07 +03:00
0cee625e8b statically link glwf on windows and ubuntu 2025-12-09 14:17:30 +03:00
dfbcdb6e1c build: updated pkgbuild depends 2025-12-09 13:32:38 +03:00
23a6a9245d build: use glfw3.4 2025-12-09 13:04:51 +03:00
93ab7bef81 ci: added wayland packages to rpm test 2025-12-09 11:54:17 +03:00
792aed7439 build: add flake.nix and wayland deps (untested) 2025-12-09 11:48:40 +03:00
38318f0205 docs: update readme 2025-12-09 01:48:22 +03:00
8a9695f3b8 ci: add sudo to deb workflow 2025-12-09 01:25:41 +03:00
dd38d08914 ci: add rpm and deb build tests 2025-12-09 01:22:27 +03:00
f55d224fab ci: add pkgbuild-git tester 2025-12-09 00:55:21 +03:00
931277291b updated gitignore 2025-12-09 00:30:09 +03:00
d8baae2ae9 build: moved pkgbuilds to AUR dir 2025-12-09 00:26:28 +03:00
5dafb6ce8c build: set deb arch 2025-12-09 00:17:50 +03:00
103 changed files with 6692 additions and 2546 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake .

45
.github/workflows/Test PKGBUILD-git.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: Test PKGBUILD-git
on:
push:
branches: master
pull_request:
branches: master
jobs:
build:
runs-on: ubuntu-latest
container: archlinux:latest
steps:
- name: Setup Arch
run: |
pacman -Sy --noconfirm --needed base-devel git sudo
useradd -m builder
echo "builder ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set permissions
run: chown -R builder:builder .
- name: Build
run: |
sudo -u builder bash -c '
cd AUR
makepkg -p PKGBUILD-git -si --noconfirm
'
- name: Test
run: |
clrsync_cli --help
pacman -Ql clrsync-git
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: clrsync-git
path: AUR/*.pkg.tar.zst

33
.github/workflows/Test flake.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Test flake.nix
on:
push:
branches: master
pull_request:
branches: master
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install Nix
uses: cachix/install-nix-action@v31
with:
extra_nix_config: |
experimental-features = nix-command flakes
- name: Build clrsync package
run: |
nix --extra-experimental-features "flakes nix-command" build .#packages.x86_64-linux.clrsync
- name: Enter devShell
run: |
nix --extra-experimental-features "flakes nix-command" develop .#default --command true
- name: Test clrsync CLI
run: |
nix --extra-experimental-features "flakes nix-command" run .#clrsync-cli -- --help

141
.github/workflows/publish-release.yml vendored Normal file
View File

@@ -0,0 +1,141 @@
name: Build and Release Packages
on:
push:
tags:
- 'v*'
jobs:
build-windows:
runs-on: windows-latest
outputs:
artifact-path: ${{ steps.upload.outputs.artifact-path }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install NSIS
run: choco install nsis --no-progress -y
- name: Setup MSVC
uses: microsoft/setup-msbuild@v2
- name: Configure project
run: cmake -B build -S . -A x64
- name: Build project
run: cmake --build build --config Release
- name: Generate NSIS installer
run: cd build && cpack -G NSIS
- name: Upload installer
id: upload
uses: actions/upload-artifact@v6
with:
name: windows-installer
path: build/*.exe
build-ubuntu:
runs-on: ubuntu-latest
outputs:
artifact-path: ${{ steps.upload.outputs.artifact-path }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y cmake build-essential git \
libglfw3-dev libfreetype6-dev libfontconfig1-dev \
zlib1g-dev libharfbuzz-dev \
libx11-dev libxrandr-dev libxi-dev libgtk-3-dev \
mesa-common-dev libgl1-mesa-dev libglu1-mesa-dev \
libxinerama-dev libxcursor-dev libxkbcommon-dev
- name: Configure CMake
run: cmake -B build -DCMAKE_BUILD_TYPE=Release -DUSE_SYSTEM_GLFW=OFF
- name: Build
run: cmake --build build --config Release
- name: Package DEB
run: cd build && cpack -G DEB
- name: Upload DEB
id: upload
uses: actions/upload-artifact@v6
with:
name: deb-package
path: build/*.deb
build-fedora:
runs-on: ubuntu-latest
container:
image: fedora:latest
outputs:
artifact-path: ${{ steps.upload.outputs.artifact-path }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install dependencies
run: |
dnf install -y cmake gcc gcc-c++ make rpm-build git \
glfw-devel freetype-devel fontconfig-devel \
zlib-devel harfbuzz-devel \
libX11-devel libXrandr-devel libXi-devel \
mesa-libGL-devel mesa-libGLU-devel \
libXinerama-devel libXcursor-devel \
wayland-devel wayland-protocols-devel gtk3-devel
- name: Configure CMake
run: cmake -B build -DCMAKE_BUILD_TYPE=Release -DUSE_SYSTEM_GLFW=ON
- name: Build
run: cmake --build build --config Release
- name: Package RPM
run: cd build && cpack -G RPM
- name: Upload RPM
id: upload
uses: actions/upload-artifact@v6
with:
name: rpm-package
path: build/*.rpm
release:
needs: [build-windows, build-ubuntu, build-fedora]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download Windows artifact
uses: actions/download-artifact@v6
with:
name: windows-installer
path: artifacts/
- name: Download DEB artifact
uses: actions/download-artifact@v6
with:
name: deb-package
path: artifacts/
- name: Download RPM artifact
uses: actions/download-artifact@v6
with:
name: rpm-package
path: artifacts/
- name: Create Release and Upload Assets
uses: ncipollo/release-action@v1
with:
tag: ${{ github.ref_name }}
name: Release ${{ github.ref_name }}
artifacts: |
artifacts/*.exe
artifacts/*.deb
artifacts/*.rpm

26
.gitignore vendored
View File

@@ -3,8 +3,21 @@
.vs .vs
out out
/build build/
/build-* build-msvc/
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
Makefile
*.cmake
AUR/clrsync-git
AUR/pkg
AUR/src
result
result-*
.direnv/
*.log *.log
*tar.zst *tar.zst
@@ -14,3 +27,12 @@ out
*.bak *.bak
*.tmp *.tmp
.DS_Store .DS_Store
*.swp
*.swo
*~
*.o
*.a
*.so
*.dylib

18
.vscode/launch.json vendored
View File

@@ -5,7 +5,7 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Debug current target", "name": "Debug current target (GDB)",
"type": "cppdbg", "type": "cppdbg",
"request": "launch", "request": "launch",
"program": "${command:cmake.launchTargetPath}", "program": "${command:cmake.launchTargetPath}",
@@ -31,6 +31,22 @@
"ignoreFailures": true "ignoreFailures": true
} }
] ]
},
{
"name": "Debug current target (LLDB)",
"type": "cppdbg",
"request": "launch",
"program": "${command:cmake.launchTargetPath}",
"args": [
"--apply",
"--theme",
"dark"
],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "lldb",
}, },
] ]
} }

View File

@@ -1,24 +1,35 @@
# Maintainer: Daniel Dada <dan@binarygoose.dev> # Maintainer: Daniel Dada <dan@binarygoose.dev>
pkgname=clrsync pkgname=clrsync
pkgver=0.1.3 pkgver=0.1.7
pkgrel=1 pkgrel=1
pkgdesc="Color scheme manager" pkgdesc="Color scheme manager"
arch=('x86_64') arch=('x86_64')
url="https://github.com/obsqrbtz/clrsync" url="https://github.com/obsqrbtz/clrsync"
license=('MIT') license=('MIT')
depends=( depends=(
glfw-x11 glfw
freetype2 freetype2
fontconfig fontconfig
mesa
libglvnd
libxcursor
gtk3
)
makedepends=(
cmake
glfw
libx11 libx11
libxrandr libxrandr
libxi libxi
mesa
libglvnd
libxinerama libxinerama
libxcursor libxcursor
wayland
wayland-protocols
gtk3
) )
makedepends=('cmake')
source=("$pkgname-$pkgver.tar.gz::https://github.com/obsqrbtz/clrsync/archive/refs/tags/v$pkgver.tar.gz") source=("$pkgname-$pkgver.tar.gz::https://github.com/obsqrbtz/clrsync/archive/refs/tags/v$pkgver.tar.gz")
sha256sums=('SKIP') sha256sums=('SKIP')
@@ -26,6 +37,7 @@ build() {
cd "$pkgname-$pkgver" cd "$pkgname-$pkgver"
cmake -B build -S . \ cmake -B build -S . \
-DCMAKE_BUILD_TYPE=Release \ -DCMAKE_BUILD_TYPE=Release \
-DUSE_SYSTEM_GLFW=ON \
-DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_PREFIX=/usr
cmake --build build cmake --build build
} }
@@ -33,5 +45,5 @@ build() {
package() { package() {
cd "$pkgname-$pkgver" cd "$pkgname-$pkgver"
DESTDIR="$pkgdir" cmake --install build DESTDIR="$pkgdir" cmake --install build
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" install -Dm644 LICENSE.txt "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
} }

57
AUR/PKGBUILD-git Normal file
View File

@@ -0,0 +1,57 @@
# Maintainer: Daniel Dada <dan@binarygoose.dev>
pkgname=clrsync-git
pkgver=r22.d8baae2
pkgrel=1
pkgdesc="Color scheme manager (git version)"
arch=('x86_64')
url="https://github.com/obsqrbtz/clrsync"
license=('MIT')
depends=(
glfw
freetype2
fontconfig
zlib
harfbuzz
mesa
libglvnd
libxcursor
gtk3
)
makedepends=(
cmake
git
glfw
libx11
libxrandr
libxi
libxinerama
libxcursor
wayland
wayland-protocols
gtk3
)
provides=('clrsync')
conflicts=('clrsync')
source=("$pkgname::git+https://github.com/obsqrbtz/clrsync.git")
sha256sums=('SKIP')
pkgver() {
cd "$srcdir/$pkgname"
printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
}
build() {
cd "$srcdir/$pkgname"
cmake -B build -S . \
-DCMAKE_BUILD_TYPE=Release \
-DUSE_SYSTEM_GLFW=ON \
-DCMAKE_INSTALL_PREFIX=/usr
cmake --build build
}
package() {
cd "$srcdir/$pkgname"
DESTDIR="$pkgdir" cmake --install build
install -Dm644 LICENSE.txt "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
}

View File

@@ -6,19 +6,30 @@ pkgdesc="Color scheme manager"
arch=('x86_64') arch=('x86_64')
url="https://github.com/obsqrbtz/clrsync" url="https://github.com/obsqrbtz/clrsync"
license=('MIT') license=('MIT')
depends=( depends=(
glfw-x11 glfw
freetype2 freetype2
fontconfig fontconfig
mesa
libglvnd
libxcursor
gtk3
)
makedepends=(
cmake
glfw
libx11 libx11
libxrandr libxrandr
libxi libxi
mesa
libglvnd
libxinerama libxinerama
libxcursor libxcursor
wayland
wayland-protocols
gtk3
) )
makedepends=('cmake')
source=("$pkgname-$pkgver.tar.gz::https://github.com/obsqrbtz/clrsync/archive/refs/tags/v$pkgver.tar.gz") source=("$pkgname-$pkgver.tar.gz::https://github.com/obsqrbtz/clrsync/archive/refs/tags/v$pkgver.tar.gz")
sha256sums=('SKIP') sha256sums=('SKIP')
@@ -26,6 +37,7 @@ build() {
cd "$pkgname-$pkgver" cd "$pkgname-$pkgver"
cmake -B build -S . \ cmake -B build -S . \
-DCMAKE_BUILD_TYPE=Release \ -DCMAKE_BUILD_TYPE=Release \
-DUSE_SYSTEM_GLFW=ON \
-DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_PREFIX=/usr
cmake --build build cmake --build build
} }
@@ -33,5 +45,5 @@ build() {
package() { package() {
cd "$pkgname-$pkgver" cd "$pkgname-$pkgver"
DESTDIR="$pkgdir" cmake --install build DESTDIR="$pkgdir" cmake --install build
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" install -Dm644 LICENSE.txt "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
} }

View File

@@ -1,11 +1,18 @@
cmake_minimum_required(VERSION 3.25) cmake_minimum_required(VERSION 3.25)
project(clrsync VERSION 0.1.3 LANGUAGES CXX) project(clrsync VERSION 0.1.7 LANGUAGES CXX)
include(GNUInstallDirs) include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
option(USE_SYSTEM_GLFW "Use system-installed GLFW instead of fetching it statically" OFF)
message(STATUS "USE_SYSTEM_GLFW: ${USE_SYSTEM_GLFW}")
if(WIN32) if(WIN32)
set(CMAKE_INSTALL_PREFIX "C:/Program Files/clrsync") set(CMAKE_INSTALL_PREFIX "C:/Program Files/clrsync")
set(CMAKE_INSTALL_BINDIR "bin") set(CMAKE_INSTALL_BINDIR "bin")
@@ -19,202 +26,67 @@ set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE)
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
set(CMAKE_INSTALL_RPATH "$ORIGIN/../${CMAKE_INSTALL_LIBDIR}") set(CMAKE_INSTALL_RPATH "$ORIGIN/../${CMAKE_INSTALL_LIBDIR}")
if(DEFINED CLRSYNC_SEMVER)
set(SEMVER "${CLRSYNC_SEMVER}")
else()
find_package(Git QUIET)
if(GIT_FOUND AND EXISTS "${CMAKE_SOURCE_DIR}/.git")
execute_process(
COMMAND git describe --tags --long --always
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_DESCRIBE
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
endif()
if(GIT_DESCRIBE MATCHES "^[vV]?[0-9]+\\.[0-9]+\\.[0-9]+-[0-9]+-g[0-9a-f]+")
string(REGEX REPLACE
"^[vV]?([0-9]+\\.[0-9]+\\.[0-9]+)-([0-9]+)-g([0-9a-f]+)"
"\\1+git.g\\3"
SEMVER "${GIT_DESCRIBE}"
)
elseif(GIT_DESCRIBE)
set(SEMVER "${PROJECT_VERSION}.git.${GIT_DESCRIBE}")
else()
set(SEMVER "${PROJECT_VERSION}")
endif()
endif()
message(STATUS "clrsync version: ${SEMVER}")
configure_file( configure_file(
${CMAKE_SOURCE_DIR}/src/core/version.hpp.in ${CMAKE_SOURCE_DIR}/src/core/common/version.hpp.in
${CMAKE_SOURCE_DIR}/src/core/version.hpp ${CMAKE_SOURCE_DIR}/src/core/common/version.hpp
@ONLY @ONLY
) )
configure_file( configure_file(
${CMAKE_SOURCE_DIR}/PKGBUILD.in ${CMAKE_SOURCE_DIR}/VERSION.in
${CMAKE_SOURCE_DIR}/PKGBUILD ${CMAKE_SOURCE_DIR}/VERSION
@ONLY @ONLY
) )
find_package(OpenGL REQUIRED) configure_file(
${CMAKE_SOURCE_DIR}/AUR/PKGBUILD.in
if(WIN32) ${CMAKE_SOURCE_DIR}/AUR/PKGBUILD
include(FetchContent) @ONLY
FetchContent_Declare(
freetype
URL https://download.savannah.gnu.org/releases/freetype/freetype-2.14.1.tar.gz
)
FetchContent_MakeAvailable(freetype)
FetchContent_Declare(
glfw
GIT_REPOSITORY https://github.com/glfw/glfw.git
GIT_TAG 3.3.10
) )
set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE) include(Dependencies)
set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE) include(ImGui)
FetchContent_MakeAvailable(glfw) add_subdirectory(src/core)
add_subdirectory(src/cli)
add_subdirectory(src/gui)
else() include(Install)
find_package(Freetype REQUIRED) include(Packaging)
find_package(PkgConfig REQUIRED)
find_package(Fontconfig REQUIRED)
pkg_check_modules(GLFW REQUIRED glfw3)
endif()
set(CORE_SOURCES
src/core/palette/color.cpp
src/core/io/toml_file.cpp
src/core/config/config.cpp
src/core/utils.cpp
src/core/version.cpp
src/core/theme/theme_template.cpp
)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
add_library(clrsync_core SHARED ${CORE_SOURCES})
target_include_directories(clrsync_core PUBLIC src SYSTEM lib)
target_compile_definitions(clrsync_core PUBLIC
CLRSYNC_DATADIR=\"${CMAKE_INSTALL_FULL_DATADIR}/clrsync\"
)
add_executable(clrsync_cli src/cli/main.cpp)
target_include_directories(clrsync_cli PRIVATE src SYSTEM lib)
target_link_libraries(clrsync_cli PRIVATE clrsync_core)
set(GUI_SOURCES
src/gui/main.cpp
src/gui/color_scheme_editor.cpp
src/gui/template_editor.cpp
src/gui/palette_controller.cpp
src/gui/template_controller.cpp
lib/color_text_edit/TextEditor.cpp
src/gui/imgui_helpers.cpp
src/gui/imgui_helpers.hpp
src/gui/about_window.cpp
src/gui/settings_window.cpp
src/gui/font_loader.cpp
)
add_executable(clrsync_gui ${GUI_SOURCES})
target_include_directories(clrsync_gui PRIVATE src SYSTEM lib)
# if(WIN32)
# set_target_properties(clrsync_gui PROPERTIES
# LINK_FLAGS "/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup"
# )
# endif()
if(WIN32)
target_link_libraries(clrsync_gui PRIVATE clrsync_core glfw freetype imgui OpenGL::GL)
else()
target_include_directories(clrsync_gui PRIVATE ${FREETYPE_INCLUDE_DIRS} ${GLFW_INCLUDE_DIRS})
target_link_libraries(clrsync_gui PRIVATE clrsync_core imgui ${FREETYPE_LIBRARIES} ${GLFW_LIBRARIES} X11 Xrandr Xi Fontconfig::Fontconfig OpenGL::GL)
endif()
set(imgui_SOURCE_DIR lib/imgui)
add_library(imgui STATIC
${imgui_SOURCE_DIR}/imgui.cpp
${imgui_SOURCE_DIR}/imgui_draw.cpp
${imgui_SOURCE_DIR}/imgui_widgets.cpp
${imgui_SOURCE_DIR}/imgui_tables.cpp
${imgui_SOURCE_DIR}/backends/imgui_impl_glfw.cpp
${imgui_SOURCE_DIR}/backends/imgui_impl_opengl3.cpp
${imgui_SOURCE_DIR}/misc/freetype/imgui_freetype.cpp
)
target_include_directories(imgui PUBLIC SYSTEM
${imgui_SOURCE_DIR}
${imgui_SOURCE_DIR}/backends
)
if(WIN32)
target_include_directories(imgui PUBLIC ${GLFW_INCLUDE_DIRS} ${freetype_SOURCE_DIR}/include)
else()
target_include_directories(imgui PUBLIC ${GLFW_INCLUDE_DIRS} ${FREETYPE_INCLUDE_DIRS})
endif()
target_link_libraries(imgui PUBLIC glfw OpenGL::GL freetype)
target_compile_definitions(imgui PUBLIC IMGUI_ENABLE_FREETYPE)
install(TARGETS clrsync_core
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
COMPONENT Core
)
install(TARGETS clrsync_cli
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
COMPONENT CLI
)
install(TARGETS clrsync_gui
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
COMPONENT GUI
)
install(FILES
example_config/config.toml
DESTINATION ${CMAKE_INSTALL_DATADIR}/clrsync
COMPONENT Core
)
install(DIRECTORY example_config/templates
DESTINATION ${CMAKE_INSTALL_DATADIR}/clrsync
COMPONENT Core
FILES_MATCHING PATTERN "*"
)
install(DIRECTORY example_config/palettes
DESTINATION ${CMAKE_INSTALL_DATADIR}/clrsync
COMPONENT Core
FILES_MATCHING PATTERN "*.toml"
)
if(UNIX AND NOT APPLE)
install(FILES resources/clrsync.desktop
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications
COMPONENT Core
)
endif()
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE.txt")
set(CPACK_COMPONENTS_ALL Core GUI CLI)
set(CPACK_PACKAGE_NAME "clrsync")
set(CPACK_PACKAGE_VENDOR "Daniel Dada")
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Color scheme manager")
set(CPACK_COMPONENTS_ALL Core GUI CLI)
set(CPACK_COMPONENT_CORE_DISPLAY_NAME "Core Library")
set(CPACK_COMPONENT_CORE_DESCRIPTION "clrsync core library and default configs (required)")
set(CPACK_COMPONENT_CORE_REQUIRED ON)
set(CPACK_COMPONENT_GUI_DISPLAY_NAME "GUI Application")
set(CPACK_COMPONENT_GUI_DESCRIPTION "clrsync GUI app")
set(CPACK_COMPONENT_GUI_DEPENDS Core)
set(CPACK_COMPONENT_CLI_DISPLAY_NAME "Command Line Tool")
set(CPACK_COMPONENT_CLI_DESCRIPTION "clrsync CLI app")
set(CPACK_COMPONENT_CLI_DEPENDS Core)
set(CPACK_GENERATOR "NSIS;DEB;RPM")
set(CPACK_NSIS_INSTALLED_NAME "clrsync")
set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES64")
set(CPACK_NSIS_MODIFY_PATH ON)
set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON)
set(CPACK_NSIS_MENU_LINKS "bin/clrsync_gui.exe" "clrsync")
set(CPACK_NSIS_CREATE_DESKTOP_LINKS "bin/clrsync_gui.exe;clrsync")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Daniel Dada <dan@binarygoose.dev>")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.31), libglfw3, libfreetype6")
set(CPACK_DEBIAN_PACKAGE_SECTION "utils")
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
set(CPACK_RPM_PACKAGE_LICENSE "MIT")
set(CPACK_RPM_PACKAGE_GROUP "Applications/System")
set(CPACK_RPM_PACKAGE_URL "https://github.com/obsqrbtz/clrsync")
set(CPACK_RPM_PACKAGE_REQUIRES "freetype, glfw, fontconfig")
message(STATUS "CMAKE_INSTALL_PREFIX: ${CMAKE_INSTALL_PREFIX}") message(STATUS "CMAKE_INSTALL_PREFIX: ${CMAKE_INSTALL_PREFIX}")
message(STATUS "CMAKE_INSTALL_FULL_DATADIR: ${CMAKE_INSTALL_FULL_DATADIR}") message(STATUS "CMAKE_INSTALL_FULL_DATADIR: ${CMAKE_INSTALL_FULL_DATADIR}")
include(CPack) message(STATUS "CMAKE_INSTALL_PREFIX: ${CMAKE_INSTALL_PREFIX}")
message(STATUS "CMAKE_INSTALL_FULL_DATADIR: ${CMAKE_INSTALL_FULL_DATADIR}")

408
README.md
View File

@@ -1,56 +1,271 @@
[![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 # clrsync
A theme management tool for synchronizing color schemes across multiple applications. clrsync allows you to define color palettes once and apply them consistently to all your terminal emulators, editors, and other configurable applications. **Notice:** This application is not yet released and is subject to change.
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.
![Preview](assets/screenshot.png) ![Preview](assets/screenshot.png)
## Table of Contents
- [Features](#features)
- [Installation](#installation)
- [Linux](#linux)
- [Ubuntu](#ubuntu)
- [Fedora](#fedora)
- [NixOS](#nixos)
- [Home Manager Module](#home-manager-module)
- [Package](#package)
- [Install to profile](#install-to-profile)
- [Run without installing](#run-without-installing)
- [Windows](#windows)
- [Other systems](#other-systems)
- [Building](#building)
- [Prerequisites](#prerequisites)
- [With CMake](#with-cmake)
- [Configuration](#configuration)
- [Palette Files](#palette-files)
- [Template Files](#template-files)
- [Color Format Specifiers](#color-format-specifiers)
- [Usage](#usage)
- [CLI](#cli)
- [GUI](#gui)
- [Acknowledgments](#acknowledgments)
## Features ## Features
- **Unified Color Management**: Define color palettes in TOML format and apply them across multiple applications - **Unified Color Management**: Define color palettes in TOML format and apply them across multiple applications
- **CLI & GUI**: Choose between a command-line interface or a graphical editor - **CLI & GUI**: Choose between a command-line interface or a graphical editor
- **Live Reload**: Define post-apply hooks (configurable per template) - **Live Reload**: Define post-apply hooks (configurable per template)
- **Flexible Color Formats**: Support for HEX, RGB, HSL with multi-component access (e.g., `{color.r}`, `{color.hex}`, `{color.hsl}`) - **Flexible Color Formats**: Support for HEX, RGB, HSL with multi-component access (e.g., `{color.r}`, `{color.hex}`, `{color.hsl}`)
- **Pre-built Themes**: Includes popular themes
## Installation
### Linux
#### Ubuntu
1. Download the latest .deb from the [releases page](https://github.com/obsqrbtz/clrsync/releases)
2. Install the package
```shell
sudo dpkg -i clrsync-<version>.deb
```
#### Fedora
1. Download the latest .rpm from the [releases page](https://github.com/obsqrbtz/clrsync/releases)
2. Install the package
```shell
sudo rpm -i clrsync-<version>.rpm
# or
sudo dnf install clrsync-<version>.rpm
```
#### NixOS
<details>
<summary>Home Manager Module</summary>
1. Add clrsync to your flake inputs
```nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
home-manager.url = "github:nix-community/home-manager";
clrsync.url = "github:obsqrbtz/clrsync";
};
}
```
2. Add clrsync to flake outputs
```nix
outputs =
{
self,
nixpkgs,
home-manager,
clrsync,
...
}@inputs:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in
{
# ...
homeConfigurations.<Your user name> = home-manager.lib.homeManagerConfiguration {
inherit pkgs;
extraSpecialArgs = { inherit inputs; };
modules = [
./home.nix
clrsync.homeModules.default
];
};
};
```
3. Configure in home manager
```nix
programs.clrsync = {
package = inputs.clrsync.packages.x86_64-linux.default;
defaultTheme = "dark";
palettesPath = "~/.config/clrsync/palettes";
font = "JetBrainsMono Nerd Font Mono";
fontSize = 14;
applyTheme = true;
templates = {
kitty = {
enabled = true;
inputPath = "~/.config/clrsync/templates/kitty.conf";
outputPath = "~/.config/kitty/clrsync.conf";
reloadCmd = "pkill -SIGUSR1 kitty";
};
rofi = {
enabled = true;
inputPath = "~/.config/clrsync/templates/rofi.rasi";
outputPath = "~/.config/rofi/clrsync.rasi";
};
};
};
```
4. Rebuild
```nix
home-manager switch --flake .
```
</details>
<details>
<summary>Package</summary>
1. Add clrsync to your flake inputs
```nix
{
inputs = {
clrsync.url = "github:obsqrbtz/clrsync";
};
}
```
2. Install the package
```nix
# In NixOS configuration.nix:
nixpkgs.overlays = [
inputs.clrsync.overlays.default
];
environment.systemPackages = [
clrsync
];
```
Or for home manager:
```nix
# flake.nix
pkgs = import nixpkgs {
inherit system;
overlays = [
clrsync.overlays.default
];
};
```
```nix
# home.nix
home.packages = [
clrsync
];
```
3. Use the app manually
```shell
clrsync_gui
# or
clrsync_cli --apply --theme dark
```
</details>
<details>
<summary>Install to profile</summary>
```shell
nix profile add github:obsqrbtz/clrsync
```
</details>
<details>
<summary>Run without installing</summary>
```shell
nix run github:obsqrbtz/clrsync
nix run github:obsqrbtz/clrsync#clrsync-cli
```
</details>
### Windows
1. Download the latest installer from the [releases page](https://github.com/obsqrbtz/clrsync/releases)
2. Run the installer and follow the wizard
3. Optionally, add the installation dir to your PATH for easier CLI access
### Other systems
Follow the steps from Building section then install with cmake:
```bash
cd build
cmake --install .
```
## Building ## Building
### Prerequisites ### Prerequisites
- C++20 compatible compiler (GCC, Clang, or MSVC) - C++20 compatible compiler (GCC, Clang, or MSVC)
- CMake or Meson - CMake
- OpenGL - OpenGL
- glfw
- fontconfig
- freetype
### Using CMake ### With CMake
```bash ```bash
mkdir build && cd build mkdir build && cd build
cmake .. cmake ..
cmake --build . cmake --build .
``` ```
### Using Meson
```bash
meson setup builddir
meson compile -C builddir
```
## Installation
After building, you'll have:
- `clrsync_cli` - CLI
- `clrsync_gui` - GUI
- `libclrsync_core` - Shared lib
## Configuration ## Configuration
Create a configuration file at `~/.config/clrsync/config.toml`: Edit or create a configuration file at `~/.config/clrsync/config.toml`:
```toml ```toml
[general] [general]
palettes_path = "~/.config/clrsync/palettes" palettes_path = "~/.config/clrsync/palettes"
default_theme = "dark" default_theme = "cursed"
[templates.kitty] [templates.kitty]
input_path = "~/.config/clrsync/templates/kitty.conf" input_path = "~/.config/clrsync/templates/kitty.conf"
@@ -61,79 +276,109 @@ reload_cmd = "pkill -SIGUSR1 kitty"
### Palette Files ### Palette Files
Create palette files in your `palettes_path` directory: <details>
<summary>Example palette file</summary>
Create palette files in your `palettes_path` directory:
```toml ```toml
# ~/.config/clrsync/palettes/dark.toml # ~/.config/clrsync/palettes/dark.toml
[general] [general]
name = "dark" name = 'cursed'
[colors] [colors]
background = "#111318FF" accent = '#B44242FF'
surface = "#1E1F25FF" background = '#151515FF'
surface_variant = "#282A2FFF" base00 = '#151515FF'
base01 = '#B44242FF'
foreground = "#E2E2E9FF" base02 = '#95A328FF'
foreground_secondary = "#A8ABB3FF" base03 = '#E1C135FF'
base04 = '#60928FFF'
accent = "#00AA56FF" base05 = '#7C435AFF'
outline = "#44474FFF" base06 = '#A48B4AFF'
shadow = "#00000080" base07 = '#C2C2B0FF'
cursor = "#FFFFFFFF" base08 = '#3F3639FF'
base09 = '#DC7671FF'
error = "#FF5F5FFF" base0A = '#E8E85AFF'
warning = "#FFC966FF" base0B = '#9E9052FF'
success = "#6AD68BFF" base0C = '#76C39BFF'
info = "#5DB2FFFF" base0D = '#86596CFF'
base0E = '#CEB34FFF'
term_black = "#111318FF" base0F = '#B0AFA8FF'
term_red = "#FF5F5FFF" border = '#3F3639FF'
term_green = "#00AA56FF" border_focused = '#E1C135FF'
term_yellow = "#FFC966FF" cursor = '#E1C135FF'
term_blue = "#5DB2FFFF" editor_background = '#151515FF'
term_magenta = "#DEBCDFFF" editor_command = '#CEB34FFF'
term_cyan = "#86C9FFFF" editor_comment = '#3F3639FF'
term_white = "#E2E2E9FF" editor_disabled = '#3F3639FF'
editor_emphasis = '#DC7671FF'
term_black_bright = "#33353AFF" editor_error = '#B44242FF'
term_red_bright = "#FFB780FF" editor_inactive = '#3F3639FF'
term_green_bright = "#00CC6AFF" editor_line_number = '#86596CFF'
term_yellow_bright = "#FFD580FF" editor_link = '#60928FFF'
term_blue_bright = "#86C9FFFF" editor_main = '#C2C2B0FF'
term_magenta_bright = "#F0D6F0FF" editor_selected = '#3F3639FF'
term_cyan_bright = "#BFEFFFFF" editor_selection_inactive = '#2A2A2AFF'
term_white_bright = "#FFFFFFFF" editor_string = '#76C39BFF'
editor_success = '#95A328FF'
editor_warning = '#E1C135FF'
error = '#B44242FF'
foreground = '#C2C2B0FF'
info = '#60928FFF'
on_background = '#C2C2B0FF'
on_error = '#151515FF'
on_info = '#151515FF'
on_success = '#151515FF'
on_surface = '#C2C2B0FF'
on_surface_variant = '#C2C2B0FF'
on_warning = '#151515FF'
success = '#95A328FF'
surface = '#1C1C1CFF'
surface_variant = '#1C1C1CFF'
warning = '#E1C135FF'
``` ```
</details>
### Template Files ### Template Files
Create template files using color variables with flexible format specifiers: <details>
<summary>Example template file</summary>
Create template files at `~/.config/clrsync/templates` using color variables:
```conf ```conf
# ~/.config/clrsync/templates/kitty.conf # ~/.config/clrsync/templates/kitty.conf
cursor {foreground} cursor {cursor}
cursor_text_color {background} cursor_text_color {background}
foreground {foreground} foreground {foreground}
background {background} background {background}
selection_foreground {foreground_secondary} selection_foreground {on_surface}
selection_background {surface} selection_background {surface}
url_color {accent} url_color {accent}
color0 {base00}
color0 {background} color8 {base08}
color1 {term_red} color1 {base01}
color2 {term_green} color9 {base09}
color3 {term_yellow} color2 {base02}
color4 {term_blue} color10 {base0A}
color5 {term_magenta} color3 {base03}
color6 {term_cyan} color11 {base0B}
color7 {term_white} color4 {base04}
color12 {base0C}
color5 {base05}
color13 {base0D}
color6 {base06}
color14 {base0E}
color7 {base07}
color15 {base0F}
``` ```
#### Color Format Specifiers </details>
Access color components using dot notation: <details>
<summary>Color Format Specifiers</summary>
Format colors using dot notation:
```conf ```conf
# HEX formats # HEX formats
{color} # Default: #RRGGBB {color} # Default: #RRGGBB
@@ -163,42 +408,38 @@ Access color components using dot notation:
{color.a} # Alpha component {color.a} # Alpha component
``` ```
</details>
## Usage ## Usage
### CLI ### CLI
List available themes: List available themes:
```bash ```bash
clrsync_cli --list-themes clrsync_cli --list-themes
``` ```
Apply the default theme: Apply the default theme:
```bash ```bash
clrsync_cli --apply clrsync_cli --apply
``` ```
Apply a specific theme: Apply a specific theme:
```bash ```bash
clrsync_cli --apply --theme rose-pine clrsync_cli --apply --theme cursed
``` ```
Apply a theme from a file path: Apply a theme from a file path:
```bash ```bash
clrsync_cli --apply --path /path/to/theme.toml clrsync_cli --apply --path /path/to/theme.toml
``` ```
Show available color variables: Show available color variables:
```bash ```bash
clrsync_cli --show-vars clrsync_cli --show-vars
``` ```
Use a custom config file: Use a custom config file:
```bash ```bash
clrsync_cli --config /path/to/config.toml --apply clrsync_cli --config /path/to/config.toml --apply
``` ```
@@ -206,7 +447,6 @@ clrsync_cli --config /path/to/config.toml --apply
### GUI ### GUI
Launch the graphical editor: Launch the graphical editor:
```bash ```bash
clrsync_gui clrsync_gui
``` ```
@@ -219,10 +459,10 @@ The GUI provides:
## Acknowledgments ## Acknowledgments
This project uses the following open-source libraries: - **[matugen](https://github.com/InioX/matugen)** - A material you color generation tool
- **[Dear ImGui](https://github.com/ocornut/imgui)** - Bloat-free graphical user interface library for C++ - **[Dear ImGui](https://github.com/ocornut/imgui)** - Bloat-free graphical user interface library for C++
- **[GLFW](https://www.glfw.org/)** - Multi-platform library for OpenGL, OpenGL ES and Vulkan development - **[GLFW](https://www.glfw.org/)** - Multi-platform library for OpenGL, OpenGL ES and Vulkan development
- **[toml++](https://github.com/marzer/tomlplusplus)** - Header-only TOML config file parser and serializer for C++17 - **[toml++](https://github.com/marzer/tomlplusplus)** - Header-only TOML config file parser and serializer for C++17
- **[argparse](https://github.com/p-ranav/argparse)** - Argument Parser for Modern C++ - **[argparse](https://github.com/p-ranav/argparse)** - Argument Parser for Modern C++
- **[ImGuiColorTextEdit](https://github.com/BalazsJako/ImGuiColorTextEdit)** - Syntax highlighting text editor for ImGui - **[ImGuiColorTextEdit](https://github.com/BalazsJako/ImGuiColorTextEdit)** - Syntax highlighting text editor for ImGui
- **cursed** by **[pyratebeard](https://pyratebeard.net)** - Color scheme

1
VERSION Normal file
View File

@@ -0,0 +1 @@
0.1.7

1
VERSION.in Normal file
View File

@@ -0,0 +1 @@
@PROJECT_VERSION@

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 KiB

After

Width:  |  Height:  |  Size: 64 KiB

94
cmake/Dependencies.cmake Normal file
View File

@@ -0,0 +1,94 @@
find_package(OpenGL REQUIRED)
if(WIN32)
include(FetchContent)
FetchContent_Declare(
freetype
URL https://download.savannah.gnu.org/releases/freetype/freetype-2.14.1.tar.gz
)
set(FT_DISABLE_ZLIB FALSE CACHE BOOL "" FORCE)
set(FT_DISABLE_BZIP2 TRUE CACHE BOOL "" FORCE)
set(FT_DISABLE_PNG TRUE CACHE BOOL "" FORCE)
set(FT_DISABLE_HARFBUZZ FALSE CACHE BOOL "" FORCE)
set(FT_DISABLE_BROTLI TRUE CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(freetype)
elseif(APPLE)
option(USE_SYSTEM_GLFW ON)
find_package(Freetype REQUIRED)
find_package(ZLIB REQUIRED)
find_package(BZip2 REQUIRED)
find_package(PNG REQUIRED)
find_package(PkgConfig QUIET)
if(PkgConfig_FOUND)
pkg_check_modules(HARFBUZZ harfbuzz)
endif()
else()
find_package(Freetype REQUIRED)
find_package(PkgConfig REQUIRED)
find_package(Fontconfig REQUIRED)
find_package(ZLIB REQUIRED)
find_package(BZip2 REQUIRED)
find_package(PNG REQUIRED)
pkg_check_modules(HARFBUZZ harfbuzz)
pkg_check_modules(WAYLAND_CLIENT wayland-client)
pkg_check_modules(WAYLAND_EGL wayland-egl)
endif()
if(LINUX)
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK3 REQUIRED gtk+-3.0)
endif()
if(USE_SYSTEM_GLFW)
if(APPLE)
find_package(glfw3 QUIET)
if(glfw3_FOUND)
set(GLFW_FOUND TRUE)
set(GLFW_LIBRARIES glfw)
else()
find_package(PkgConfig REQUIRED)
pkg_check_modules(GLFW REQUIRED glfw3)
endif()
else()
pkg_check_modules(GLFW REQUIRED glfw3)
endif()
else()
include(FetchContent)
FetchContent_Declare(
glfw
GIT_REPOSITORY https://github.com/glfw/glfw.git
GIT_TAG 3.4
)
set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(GLFW_INSTALL OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(glfw)
set(GLFW_FOUND TRUE)
set(GLFW_INCLUDE_DIRS ${glfw_SOURCE_DIR}/include)
set(GLFW_LIBRARIES glfw)
endif()
set(FREETYPE_EXTRA_LIBS "")
if(HARFBUZZ_FOUND)
list(APPEND FREETYPE_EXTRA_LIBS ${HARFBUZZ_LIBRARIES})
message(STATUS "Found HarfBuzz")
endif()
set(WAYLAND_LIBS "")
if(NOT APPLE)
if(WAYLAND_CLIENT_FOUND)
list(APPEND WAYLAND_LIBS ${WAYLAND_CLIENT_LIBRARIES})
message(STATUS "Found Wayland client")
endif()
if(WAYLAND_EGL_FOUND)
list(APPEND WAYLAND_LIBS ${WAYLAND_EGL_LIBRARIES})
message(STATUS "Found Wayland EGL")
endif()
endif()

32
cmake/ImGui.cmake Normal file
View File

@@ -0,0 +1,32 @@
set(IMGUI_SOURCE_DIR ${CMAKE_SOURCE_DIR}/lib/imgui)
add_library(imgui STATIC
${IMGUI_SOURCE_DIR}/imgui.cpp
${IMGUI_SOURCE_DIR}/imgui_draw.cpp
${IMGUI_SOURCE_DIR}/imgui_widgets.cpp
${IMGUI_SOURCE_DIR}/imgui_tables.cpp
${IMGUI_SOURCE_DIR}/backends/imgui_impl_glfw.cpp
${IMGUI_SOURCE_DIR}/backends/imgui_impl_opengl3.cpp
${IMGUI_SOURCE_DIR}/misc/freetype/imgui_freetype.cpp
)
target_include_directories(imgui PUBLIC SYSTEM
${IMGUI_SOURCE_DIR}
${IMGUI_SOURCE_DIR}/backends
)
target_compile_definitions(imgui PUBLIC IMGUI_ENABLE_FREETYPE)
if(WIN32)
target_include_directories(imgui PUBLIC ${GLFW_INCLUDE_DIRS} ${freetype_SOURCE_DIR}/include)
target_link_libraries(imgui PUBLIC freetype)
else()
target_include_directories(imgui PUBLIC ${GLFW_INCLUDE_DIRS} ${FREETYPE_INCLUDE_DIRS})
target_link_libraries(imgui PRIVATE
Freetype::Freetype
${FREETYPE_EXTRA_LIBS}
ZLIB::ZLIB
BZip2::BZip2
PNG::PNG
)
endif()

39
cmake/Install.cmake Normal file
View File

@@ -0,0 +1,39 @@
install(TARGETS clrsync_core
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
COMPONENT Core
)
install(TARGETS clrsync_cli
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
COMPONENT CLI
)
install(TARGETS clrsync_gui
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
COMPONENT GUI
)
install(FILES example_config/config.toml
DESTINATION ${CMAKE_INSTALL_DATADIR}/clrsync
COMPONENT Core
)
install(DIRECTORY example_config/templates
DESTINATION ${CMAKE_INSTALL_DATADIR}/clrsync
COMPONENT Core
FILES_MATCHING PATTERN "*"
)
install(DIRECTORY example_config/palettes
DESTINATION ${CMAKE_INSTALL_DATADIR}/clrsync
COMPONENT Core
FILES_MATCHING PATTERN "*.toml"
)
if(UNIX AND NOT APPLE)
install(FILES resources/clrsync.desktop
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications
COMPONENT Core
)
endif()

41
cmake/Packaging.cmake Normal file
View File

@@ -0,0 +1,41 @@
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE.txt")
set(CPACK_PACKAGE_NAME "clrsync")
set(CPACK_PACKAGE_VENDOR "Daniel Dada")
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Color scheme manager")
set(CPACK_GENERATOR "NSIS;DEB;RPM")
# Components
set(CPACK_COMPONENTS_ALL Core GUI CLI)
set(CPACK_COMPONENT_CORE_DISPLAY_NAME "Core Library")
set(CPACK_COMPONENT_CORE_DESCRIPTION "clrsync core library and default configs (required)")
set(CPACK_COMPONENT_CORE_REQUIRED ON)
set(CPACK_COMPONENT_GUI_DISPLAY_NAME "GUI Application")
set(CPACK_COMPONENT_GUI_DESCRIPTION "clrsync GUI app")
set(CPACK_COMPONENT_GUI_DEPENDS Core)
set(CPACK_COMPONENT_CLI_DISPLAY_NAME "Command Line Tool")
set(CPACK_COMPONENT_CLI_DESCRIPTION "clrsync CLI app")
set(CPACK_COMPONENT_CLI_DEPENDS Core)
# NSIS
set(CPACK_NSIS_INSTALLED_NAME "clrsync")
set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES64")
set(CPACK_NSIS_MODIFY_PATH ON)
set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON)
set(CPACK_NSIS_MENU_LINKS "bin/clrsync_gui.exe" "clrsync")
set(CPACK_NSIS_CREATE_DESKTOP_LINKS "bin/clrsync_gui.exe;clrsync")
# Debian
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Daniel Dada <dan@binarygoose.dev>")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.31), libglfw3, libfreetype6, zlib1g, libharfbuzz0b")
set(CPACK_DEBIAN_PACKAGE_SECTION "utils")
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "amd64")
# RPM
set(CPACK_RPM_PACKAGE_LICENSE "MIT")
set(CPACK_RPM_PACKAGE_GROUP "Applications/System")
set(CPACK_RPM_PACKAGE_URL "https://github.com/obsqrbtz/clrsync")
set(CPACK_RPM_PACKAGE_REQUIRES "freetype, glfw, fontconfig, zlib, harfbuzz")
include(CPack)

View File

@@ -1,5 +1,5 @@
[general] [general]
default_theme = 'cursed' default_theme = 'dark'
palettes_path = '~/.config/clrsync/palettes' palettes_path = '~/.config/clrsync/palettes'
font = 'JetBrainsMono Nerd Font Mono' font = 'JetBrainsMono Nerd Font Mono'
font_size = 14 font_size = 14

View File

@@ -1,67 +0,0 @@
[colors]
# General
background = "#f5f5f5FF"
on_background = "#3d3d2fFF"
surface = "#e8e8e8FF"
on_surface = "#3d3d2fFF"
surface_variant = "#d0d0c8FF"
on_surface_varuant = "#3d3d2fFF"
border_focused = "#c9a305FF"
border = "#d0d0c8FF"
foreground = "#3d3d2fFF"
cursor = "#c9a305FF"
accent = "#b44242FF"
# Terminal
base00 = "#f5f5f5FF"
base01 = "#b44242FF"
base02 = "#95a328FF"
base03 = "#c9a305FF"
base04 = "#60928fFF"
base05 = "#7c435aFF"
base06 = "#a48b4aFF"
base07 = "#3d3d2fFF"
base08 = "#c0c0b8FF"
base09 = "#dc7671FF"
base0A = "#d4d430FF"
base0B = "#9e9052FF"
base0C = "#76c39bFF"
base0D = "#86596cFF"
base0E = "#b89a1fFF"
base0F = "#4f4f48FF"
# Semantic
success = "#95a328FF"
info = "#60928fFF"
warning = "#c9a305FF"
error = "#b44242FF"
on_success = "#f5f5f5FF"
on_info = "#f5f5f5FF"
on_warning = "#f5f5f5FF"
on_error = "#f5f5f5FF"
# Code editor
editor_background = "#f5f5f5FF"
editor_command = "#b89a1fFF"
editor_comment = "#a0a098FF"
editor_disabled = "#c0c0b8FF"
editor_emphasis = "#dc7671FF"
editor_error = "#b44242FF"
editor_inactive = "#a0a098FF"
editor_line_number = "#86596cFF"
editor_link = "#60928fFF"
editor_main = "#3d3d2fFF"
editor_selected = "#d0d0c8FF"
editor_selection_inactive = "#e0e0d8FF"
editor_string = "#5fa37bFF"
editor_success = "#95a328FF"
editor_warning = "#c9a305FF"
[general]
name = 'cursed-light'

View File

@@ -0,0 +1,54 @@
[colors]
accent = '#9A8652FF'
background = '#111111FF'
base00 = '#111111FF'
base01 = '#668A51FF'
base02 = '#9A8652FF'
base03 = '#B47837FF'
base04 = '#9A5552FF'
base05 = '#AA477BFF'
base06 = '#3A898CFF'
base07 = '#B5B5B5FF'
base08 = '#AA4E4AFF'
base09 = '#A9DC86FF'
base0A = '#B6AB82FF'
base0B = '#C5916BFF'
base0C = '#AC7676FF'
base0D = '#B0779EFF'
base0E = '#849899FF'
base0F = '#D2D2D2FF'
border = '#242424FF'
border_focused = '#2E2E2EFF'
cursor = '#D2D2D2FF'
editor_background = '#111111FF'
editor_command = '#3A898CFF'
editor_comment = '#849899FF'
editor_disabled = '#849899FF'
editor_emphasis = '#A9DC86FF'
editor_error = '#AA4E4AFF'
editor_inactive = '#849899FF'
editor_line_number = '#849899FF'
editor_link = '#B0779EFF'
editor_main = '#D2D2D2FF'
editor_selected = '#242424FF'
editor_selection_inactive = '#1D1C1CFF'
editor_string = '#9A8652FF'
editor_success = '#668A51FF'
editor_warning = '#B47837FF'
error = '#AA4E4AFF'
foreground = '#D2D2D2FF'
info = '#3A898CFF'
on_background = '#D4D4D4FF'
on_error = '#D2D2D2FF'
on_info = '#D2D2D2FF'
on_success = '#D2D2D2FF'
on_surface = '#D4D4D4FF'
on_surface_variant = '#D4D4D4FF'
on_warning = '#D2D2D2FF'
success = '#668A51FF'
surface = '#111111FF'
surface_variant = '#191919FF'
warning = '#B47837FF'
[general]
name = 'dark'

View File

@@ -0,0 +1,54 @@
[colors]
accent = '#9A8652FF'
background = '#E0E0E0FF'
base00 = '#E0E0E0FF'
base01 = '#668A51FF'
base02 = '#9A8652FF'
base03 = '#B47837FF'
base04 = '#9A5552FF'
base05 = '#AA477BFF'
base06 = '#3A898CFF'
base07 = '#5A5A5AFF'
base08 = '#AA4E4AFF'
base09 = '#4A7A2EFF'
base0A = '#7A6A42FF'
base0B = '#A5714BFF'
base0C = '#8C5656FF'
base0D = '#90577EFF'
base0E = '#2A6A6DFF'
base0F = '#2A2A2AFF'
border = '#C5C5C5FF'
border_focused = '#B9B9B9FF'
cursor = '#2A2A2AFF'
editor_background = '#E0E0E0FF'
editor_command = '#3A898CFF'
editor_comment = '#849899FF'
editor_disabled = '#A0A0A0FF'
editor_emphasis = '#4A7A2EFF'
editor_error = '#AA4E4AFF'
editor_inactive = '#A0A0A0FF'
editor_line_number = '#9A9A95FF'
editor_link = '#90577EFF'
editor_main = '#2A2A2AFF'
editor_selected = '#C9C9C9FF'
editor_selection_inactive = '#D2D2D2FF'
editor_string = '#9A8652FF'
editor_success = '#668A51FF'
editor_warning = '#B47837FF'
error = '#AA4E4AFF'
foreground = '#2A2A2AFF'
info = '#3A898CFF'
on_background = '#2A2A2AFF'
on_error = '#FAFAF8FF'
on_info = '#FAFAF8FF'
on_success = '#FAFAF8FF'
on_surface = '#2A2A2AFF'
on_surface_variant = '#3A3A3AFF'
on_warning = '#FAFAF8FF'
success = '#668A51FF'
surface = '#E0E0E0FF'
surface_variant = '#CECECEFF'
warning = '#B47837FF'
[general]
name = 'light'

View File

@@ -7,34 +7,26 @@ selection_foreground {on_surface}
selection_background {surface} selection_background {surface}
url_color {accent} url_color {accent}
# Base colors (dark variants)
color0 {base00} color0 {base00}
color8 {base08} color8 {base08}
# Red
color1 {base01} color1 {base01}
color9 {base09} color9 {base09}
# Green
color2 {base02} color2 {base02}
color10 {base0A} color10 {base0A}
# Yellow
color3 {base03} color3 {base03}
color11 {base0B} color11 {base0B}
# Blue
color4 {base04} color4 {base04}
color12 {base0C} color12 {base0C}
# Magenta
color5 {base05} color5 {base05}
color13 {base0D} color13 {base0D}
# Cyan
color6 {base06} color6 {base06}
color14 {base0E} color14 {base0E}
# White
color7 {base07} color7 {base07}
color15 {base0F} color15 {base0F}

View File

@@ -0,0 +1,54 @@
[colors]
accent = '#95A328FF'
background = '#F5F5F5FF'
base00 = '#F5F5F5FF'
base01 = '#B44242FF'
base02 = '#95A328FF'
base03 = '#C9A305FF'
base04 = '#60928FFF'
base05 = '#7C435AFF'
base06 = '#A48B4AFF'
base07 = '#3D3D2FFF'
base08 = '#C0C0B8FF'
base09 = '#DC7671FF'
base0A = '#D4D430FF'
base0B = '#9E9052FF'
base0C = '#76C39BFF'
base0D = '#86596CFF'
base0E = '#B89A1FFF'
base0F = '#4F4F48FF'
border = '#D0D0C8FF'
border_focused = '#C9A305FF'
cursor = '#C9A305FF'
editor_background = '#F5F5F5FF'
editor_command = '#B89A1FFF'
editor_comment = '#A0A098FF'
editor_disabled = '#C0C0B8FF'
editor_emphasis = '#DC7671FF'
editor_error = '#B44242FF'
editor_inactive = '#A0A098FF'
editor_line_number = '#86596CFF'
editor_link = '#60928FFF'
editor_main = '#3D3D2FFF'
editor_selected = '#D0D0C8FF'
editor_selection_inactive = '#E0E0D8FF'
editor_string = '#5FA37BFF'
editor_success = '#95A328FF'
editor_warning = '#C9A305FF'
error = '#B44242FF'
foreground = '#3D3D2FFF'
info = '#60928FFF'
on_background = '#3D3D2FFF'
on_error = '#F5F5F5FF'
on_info = '#F5F5F5FF'
on_success = '#F5F5F5FF'
on_surface = '#3D3D2FFF'
on_surface_variant = '#CCCCCCFF'
on_warning = '#F5F5F5FF'
success = '#95A328FF'
surface = '#E8E8E8FF'
surface_variant = '#D0D0C8FF'
warning = '#C9A305FF'
[general]
name = 'cursed-light'

View File

@@ -1,5 +1,5 @@
[colors] [colors]
accent = '#B44242FF' accent = '#95A328FF'
background = '#151515FF' background = '#151515FF'
base00 = '#151515FF' base00 = '#151515FF'
base01 = '#B44242FF' base01 = '#B44242FF'
@@ -22,7 +22,7 @@ border_focused = '#E1C135FF'
cursor = '#E1C135FF' cursor = '#E1C135FF'
editor_background = '#151515FF' editor_background = '#151515FF'
editor_command = '#CEB34FFF' editor_command = '#CEB34FFF'
editor_comment = '#3F3639FF' editor_comment = '#7A7A7AFF'
editor_disabled = '#3F3639FF' editor_disabled = '#3F3639FF'
editor_emphasis = '#DC7671FF' editor_emphasis = '#DC7671FF'
editor_error = '#B44242FF' editor_error = '#B44242FF'
@@ -43,7 +43,7 @@ on_error = '#151515FF'
on_info = '#151515FF' on_info = '#151515FF'
on_success = '#151515FF' on_success = '#151515FF'
on_surface = '#C2C2B0FF' on_surface = '#C2C2B0FF'
on_surface_varuant = '#C2C2B0FF' on_surface_variant = '#CCCCCCFF'
on_warning = '#151515FF' on_warning = '#151515FF'
success = '#95A328FF' success = '#95A328FF'
surface = '#1C1C1CFF' surface = '#1C1C1CFF'

54
extra/palettes/nord.toml Normal file
View File

@@ -0,0 +1,54 @@
[colors]
accent = '#5E81ACFF'
background = '#2E3440FF'
base00 = '#2E3440FF'
base01 = '#BF616AFF'
base02 = '#A3BE8CFF'
base03 = '#EBCB8BFF'
base04 = '#81A1C1FF'
base05 = '#B48EADFF'
base06 = '#88C0D0FF'
base07 = '#E5E9F0FF'
base08 = '#4C566AFF'
base09 = '#D08770FF'
base0A = '#EBCB8BFF'
base0B = '#A3BE8CFF'
base0C = '#8FBCBBFF'
base0D = '#5E81ACFF'
base0E = '#B48EADFF'
base0F = '#ECEFF4FF'
border = '#4C566AFF'
border_focused = '#88C0D0FF'
cursor = '#D8DEE9FF'
editor_background = '#2E3440FF'
editor_command = '#81A1C1FF'
editor_comment = '#616E88FF'
editor_disabled = '#4C566AFF'
editor_emphasis = '#B48EADFF'
editor_error = '#BF616AFF'
editor_inactive = '#616E88FF'
editor_line_number = '#4C566AFF'
editor_link = '#88C0D0FF'
editor_main = '#D8DEE9FF'
editor_selected = '#434C5EFF'
editor_selection_inactive = '#3B4252FF'
editor_string = '#A3BE8CFF'
editor_success = '#A3BE8CFF'
editor_warning = '#EBCB8BFF'
error = '#BF616AFF'
foreground = '#D8DEE9FF'
info = '#5E81ACFF'
on_background = '#D8DEE9FF'
on_error = '#2E3440FF'
on_info = '#2E3440FF'
on_success = '#2E3440FF'
on_surface = '#ECEFF4FF'
on_surface_variant = '#ECEFF4FF'
on_warning = '#2E3440FF'
success = '#A3BE8CFF'
surface = '#3B4252FF'
surface_variant = '#434C5EFF'
warning = '#EBCB8BFF'
[general]
name = 'nord'

27
flake.lock generated Normal file
View File

@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1765779637,
"narHash": "sha256-KJ2wa/BLSrTqDjbfyNx70ov/HdgNBCBBSQP3BIzKnv4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1306659b587dc277866c7b69eb97e5f07864d8c4",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

94
flake.nix Normal file
View File

@@ -0,0 +1,94 @@
{
description = "clrsync - Color scheme manager";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs =
{ self, nixpkgs, ... }:
let
supportedSystems = [
"x86_64-linux"
];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
baseVersion = nixpkgs.lib.removeSuffix "\n" (builtins.readFile ./VERSION);
semver =
if self ? rev then
"${baseVersion}+git.${builtins.substring 0 7 self.rev}"
else
"${baseVersion}+dev";
in
{
packages = forAllSystems (
system:
let
pkgs = nixpkgsFor.${system};
in
rec {
clrsync = pkgs.callPackage ./package.nix { inherit semver; };
default = clrsync;
}
);
homeModules = {
default = import ./home-manager-module.nix self;
clrsync = self.homeModules.default;
};
apps = forAllSystems (system: {
clrsync-gui = {
type = "app";
program = "${self.packages.${system}.clrsync}/bin/clrsync_gui";
meta = {
description = "clrsync gui app";
license = self.packages.x86_64-linux.licenses.mit;
maintainers = [ "Daniel Dada" ];
};
};
clrsync-cli = {
type = "app";
program = "${self.packages.${system}.clrsync}/bin/clrsync_cli";
meta = {
description = "clrsync cli app";
license = self.packages.x86_64-linux.licenses.mit;
maintainers = [ "Daniel Dada" ];
};
};
default = self.apps.${system}.clrsync-cli;
});
devShells = forAllSystems (
system:
let
pkgs = nixpkgsFor.${system};
clrsync = self.packages.${system}.clrsync;
in
{
default = pkgs.mkShell {
inputsFrom = [ clrsync ];
packages = with pkgs; [
cmake
ninja
clang-tools
gdb
];
shellHook = ''
export CMAKE_GENERATOR="Ninja"
export CMAKE_EXPORT_COMPILE_COMMANDS=1
'';
};
}
);
overlays.default = final: prev: {
clrsync = self.packages.${final.system}.clrsync;
};
};
}

155
home-manager-module.nix Normal file
View File

@@ -0,0 +1,155 @@
flake:
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.programs.clrsync;
clrsyncPackage = flake.packages.${pkgs.system}.default;
templateType = types.submodule {
options = {
enabled = mkOption {
type = types.bool;
default = true;
description = "Whether to enable this template.";
};
inputPath = mkOption {
type = types.str;
description = "Path to the template input file.";
};
outputPath = mkOption {
type = types.str;
description = "Path where the generated output will be written.";
};
reloadCmd = mkOption {
type = types.str;
default = "";
description = "Command to run after generating the output.";
};
};
};
configFormat = pkgs.formats.toml { };
configFile = configFormat.generate "config.toml" {
general = {
default_theme = cfg.defaultTheme;
palettes_path = cfg.palettesPath;
font = cfg.font;
font_size = cfg.fontSize;
};
templates = mapAttrs (
name: template: {
enabled = template.enabled;
input_path = template.inputPath;
output_path = template.outputPath;
reload_cmd = template.reloadCmd;
}
) cfg.templates;
};
in
{
options.programs.clrsync = {
enable = mkEnableOption "clrsync color synchronization";
defaultTheme = mkOption {
type = types.str;
default = "cursed";
description = "Default theme to use.";
};
palettesPath = mkOption {
type = types.str;
default = "~/.config/clrsync/palettes";
description = "Path to color palettes directory.";
};
font = mkOption {
type = types.str;
default = "JetBrainsMono Nerd Font Mono";
description = "Font family to use.";
};
fontSize = mkOption {
type = types.int;
default = 14;
description = "Font size.";
};
templates = mkOption {
type = types.attrsOf templateType;
default = { };
description = "Template configurations.";
example = literalExpression ''
{
kitty = {
enabled = true;
inputPath = "~/.config/clrsync/templates/kitty.conf";
outputPath = "~/.config/kitty/kitty_test.conf";
reloadCmd = "pkill -SIGUSR1 kitty";
};
}
'';
};
applyTheme = mkOption {
type = types.bool;
default = false;
description = "Whether to apply the default theme on activation.";
};
systemdTarget = mkOption {
type = types.str;
default = "graphical-session.target";
description = "Systemd target to bind the clrsync service to.";
};
};
config = mkIf cfg.enable {
home.packages = [ clrsyncPackage ];
xdg.enable = true;
home.activation.clrsyncDesktop = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
if [ -d "$HOME/.nix-profile/share/applications" ]; then
${pkgs.desktop-file-utils}/bin/update-desktop-database "$HOME/.nix-profile/share/applications" || true
fi
'';
xdg.configFile."clrsync/config.toml" = {
source = configFile;
force = true;
};
home.activation.clrsyncConfig = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
run --quiet mkdir -p $HOME/.config/clrsync
run --quiet cp -f ${configFile} $HOME/.config/clrsync/config.toml
'';
home.activation.clrsyncApply = mkIf cfg.applyTheme (
lib.hm.dag.entryAfter [ "clrsyncConfig" ] ''
run --quiet ${clrsyncPackage}/bin/clrsync_cli --apply --theme ${cfg.defaultTheme}
''
);
systemd.user.services.clrsync = mkIf cfg.applyTheme {
Unit = {
Description = "Apply clrsync color palette";
After = [ cfg.systemdTarget ];
PartOf = [ cfg.systemdTarget ];
};
Service = {
Type = "oneshot";
ExecStart = "${clrsyncPackage}/bin/clrsync_cli --apply --theme ${cfg.defaultTheme}";
RemainAfterExit = true;
};
Install = {
WantedBy = [ cfg.systemdTarget ];
};
};
};
}

104
package.nix Normal file
View File

@@ -0,0 +1,104 @@
{
lib,
stdenv,
cmake,
git,
pkg-config,
makeWrapper,
wrapGAppsHook3,
wayland-protocols,
glfw,
freetype,
fontconfig,
mesa,
xorg,
wayland,
libxkbcommon,
zlib,
bzip2,
wayland-scanner,
gtk3,
glib,
gsettings-desktop-schemas,
semver,
}:
stdenv.mkDerivation rec {
pname = "clrsync";
version = semver;
src = lib.cleanSourceWith {
src = ./.;
filter =
path: type:
let
baseName = baseNameOf path;
in
!(
lib.hasSuffix ".o" baseName
|| lib.hasSuffix ".a" baseName
|| baseName == "build"
|| baseName == "CMakeCache.txt"
|| baseName == "CMakeFiles"
|| baseName == ".git"
|| baseName == "result"
|| baseName == ".direnv"
);
};
nativeBuildInputs = [
cmake
git
pkg-config
makeWrapper
wrapGAppsHook3
wayland-protocols
];
buildInputs = [
glfw
freetype
fontconfig
xorg.libXcursor
mesa
xorg.libX11
xorg.libXrandr
xorg.libXi
xorg.libXinerama
wayland
wayland-scanner
wayland-protocols
libxkbcommon
zlib
bzip2
gtk3
gsettings-desktop-schemas
glib
];
cmakeFlags = [
"-DCMAKE_BUILD_TYPE=Release"
"-DUSE_SYSTEM_GLFW=ON"
"-DCLRSYNC_SEMVER=${version}"
];
installPhase = ''
runHook preInstall
cmake --install . --prefix $out
runHook postInstall
'';
dontWrapGApps = false;
meta = with lib; {
description = "Color scheme manager with GUI and CLI";
homepage = "https://github.com/obsqrbtz/clrsync";
license = licenses.mit;
platforms = platforms.linux;
mainProgram = "clrsync_gui";
maintainers = [ "Daniel Dada" ];
};
}

8
src/cli/CMakeLists.txt Normal file
View File

@@ -0,0 +1,8 @@
add_executable(clrsync_cli main.cpp)
target_include_directories(clrsync_cli PRIVATE
${CMAKE_SOURCE_DIR}/src
SYSTEM ${CMAKE_SOURCE_DIR}/lib
)
target_link_libraries(clrsync_cli PRIVATE clrsync_core)

View File

@@ -1,17 +1,18 @@
#include <iostream>
#include <cstdlib> #include <cstdlib>
#include <iostream>
#include <string> #include <string>
#include <argparse/argparse.hpp> #include <argparse/argparse.hpp>
#include <core/utils.hpp> #include "core/common/error.hpp"
#include <core/config/config.hpp> #include "core/common/utils.hpp"
#include <core/io/toml_file.hpp> #include "core/common/version.hpp"
#include <core/palette/palette_file.hpp> #include "core/config/config.hpp"
#include <core/palette/palette_manager.hpp> #include "core/io/toml_file.hpp"
#include <core/theme/theme_template.hpp> #include "core/palette/palette_file.hpp"
#include <core/theme/theme_renderer.hpp> #include "core/palette/palette_manager.hpp"
#include <core/version.hpp> #include "core/theme/theme_renderer.hpp"
#include "core/theme/theme_template.hpp"
void handle_show_vars() void handle_show_vars()
{ {
@@ -21,8 +22,7 @@ void handle_show_vars()
void handle_list_themes() void handle_list_themes()
{ {
auto palette_manager = clrsync::core::palette_manager<clrsync::core::io::toml_file>(); auto palette_manager = clrsync::core::palette_manager<clrsync::core::io::toml_file>();
palette_manager.load_palettes_from_directory( palette_manager.load_palettes_from_directory(clrsync::core::config::instance().palettes_path());
clrsync::core::config::instance().palettes_path());
const auto &palettes = palette_manager.palettes(); const auto &palettes = palette_manager.palettes();
std::cout << "Available themes:" << std::endl; std::cout << "Available themes:" << std::endl;
@@ -36,16 +36,17 @@ int handle_apply_theme(const argparse::ArgumentParser &program, const std::strin
{ {
clrsync::core::theme_renderer<clrsync::core::io::toml_file> renderer; clrsync::core::theme_renderer<clrsync::core::io::toml_file> renderer;
std::string theme_identifier; std::string theme_identifier;
clrsync::core::Result<void> result = clrsync::core::Ok();
if (program.is_used("--theme")) if (program.is_used("--theme"))
{ {
theme_identifier = program.get<std::string>("--theme"); theme_identifier = program.get<std::string>("--theme");
renderer.apply_theme(theme_identifier); result = renderer.apply_theme(theme_identifier);
} }
else if (program.is_used("--path")) else if (program.is_used("--path"))
{ {
theme_identifier = program.get<std::string>("--path"); theme_identifier = program.get<std::string>("--path");
renderer.apply_theme_from_path(theme_identifier); result = renderer.apply_theme_from_path(theme_identifier);
} }
else else
{ {
@@ -55,43 +56,41 @@ int handle_apply_theme(const argparse::ArgumentParser &program, const std::strin
return 1; return 1;
} }
theme_identifier = default_theme; theme_identifier = default_theme;
renderer.apply_theme(theme_identifier); result = renderer.apply_theme(theme_identifier);
}
if (!result)
{
std::cerr << "Failed to apply theme: " << result.error().description() << std::endl;
return 1;
} }
std::cout << "Applied theme " << theme_identifier << std::endl; std::cout << "Applied theme " << theme_identifier << std::endl;
return 0; return 0;
} }
void initialize_config(const std::string &config_path) clrsync::core::Result<void> initialize_config(const std::string &config_path)
{ {
auto conf = std::make_unique<clrsync::core::io::toml_file>(config_path); auto conf = std::make_unique<clrsync::core::io::toml_file>(config_path);
clrsync::core::config::instance().initialize(std::move(conf)); return clrsync::core::config::instance().initialize(std::move(conf));
} }
void setup_argument_parser(argparse::ArgumentParser &program) void setup_argument_parser(argparse::ArgumentParser &program)
{ {
program.add_argument("-a", "--apply") program.add_argument("-a", "--apply").help("applies default theme").flag();
.help("applies default theme")
.flag();
program.add_argument("-c", "--config") program.add_argument("-c", "--config")
.default_value(clrsync::core::get_default_config_path()) .default_value(clrsync::core::get_default_config_path())
.help("sets config file path") .help("sets config file path")
.metavar("PATH"); .metavar("PATH");
program.add_argument("-l", "--list-themes") program.add_argument("-l", "--list-themes").help("lists available themes").flag();
.help("lists available themes")
.flag();
program.add_argument("-s", "--show-vars") program.add_argument("-s", "--show-vars").help("shows color keys").flag();
.help("shows color keys")
.flag();
auto &group = program.add_mutually_exclusive_group(); auto &group = program.add_mutually_exclusive_group();
group.add_argument("-t", "--theme") group.add_argument("-t", "--theme").help("sets theme <theme_name> to apply");
.help("sets theme <theme_name> to apply"); group.add_argument("-p", "--path").help("sets theme file <path/to/theme> to apply");
group.add_argument("-p", "--path")
.help("sets theme file <path/to/theme> to apply");
} }
int main(int argc, char *argv[]) int main(int argc, char *argv[])
@@ -112,13 +111,10 @@ int main(int argc, char *argv[])
std::string config_path = program.get<std::string>("--config"); std::string config_path = program.get<std::string>("--config");
try auto config_result = initialize_config(config_path);
if (!config_result)
{ {
initialize_config(config_path); std::cerr << "Error loading config: " << config_result.error().description() << std::endl;
}
catch (const std::exception &err)
{
std::cerr << "Error loading config: " << err.what() << std::endl;
return 1; return 1;
} }

21
src/core/CMakeLists.txt Normal file
View File

@@ -0,0 +1,21 @@
set(CORE_SOURCES
palette/color.cpp
io/toml_file.cpp
config/config.cpp
common/utils.cpp
common/version.cpp
theme/theme_template.cpp
)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
add_library(clrsync_core SHARED ${CORE_SOURCES})
target_include_directories(clrsync_core PUBLIC
${CMAKE_SOURCE_DIR}/src
SYSTEM ${CMAKE_SOURCE_DIR}/lib
)
target_compile_definitions(clrsync_core PUBLIC
CLRSYNC_DATADIR=\"${CMAKE_INSTALL_FULL_DATADIR}/clrsync\"
)

266
src/core/common/error.hpp Normal file
View File

@@ -0,0 +1,266 @@
#ifndef CLRSYNC_CORE_ERROR_HPP
#define CLRSYNC_CORE_ERROR_HPP
#include <optional>
#include <string>
#include <variant>
namespace clrsync::core
{
enum class error_code
{
unknown,
file_not_found,
file_open_failed,
file_write_failed,
file_read_failed,
dir_create_failed,
parse_failed,
invalid_format,
config_missing,
config_invalid,
template_not_found,
template_load_failed,
template_apply_failed,
palette_not_found,
palette_load_failed,
init_failed,
invalid_arg,
resource_missing,
};
inline const char *error_code_string(error_code code)
{
switch (code)
{
case error_code::unknown:
return "Unknown error";
case error_code::file_not_found:
return "File not found";
case error_code::file_open_failed:
return "Failed to open file";
case error_code::file_write_failed:
return "Failed to write file";
case error_code::file_read_failed:
return "Failed to read file";
case error_code::dir_create_failed:
return "Failed to create directory";
case error_code::parse_failed:
return "Parse failed";
case error_code::invalid_format:
return "Invalid format";
case error_code::config_missing:
return "Configuration missing";
case error_code::config_invalid:
return "Configuration invalid";
case error_code::template_not_found:
return "Template not found";
case error_code::template_load_failed:
return "Failed to load template";
case error_code::template_apply_failed:
return "Failed to apply template";
case error_code::palette_not_found:
return "Palette not found";
case error_code::palette_load_failed:
return "Failed to load palette";
case error_code::init_failed:
return "Initialization failed";
case error_code::invalid_arg:
return "Invalid argument";
case error_code::resource_missing:
return "Resource missing";
default:
return "Unknown error code";
}
}
struct Error
{
error_code code;
std::string message;
std::string context;
Error(error_code c) : code(c), message(error_code_string(c))
{
}
Error(error_code c, std::string msg) : code(c), message(std::move(msg))
{
}
Error(error_code c, std::string msg, std::string ctx)
: code(c), message(std::move(msg)), context(std::move(ctx))
{
}
std::string description() const
{
if (context.empty())
return message;
return message + " [" + context + "]";
}
};
template <typename T> class [[nodiscard]] Result
{
private:
std::variant<T, Error> m_data;
public:
Result(T value) : m_data(std::move(value))
{
}
Result(Error error) : m_data(std::move(error))
{
}
bool is_ok() const
{
return std::holds_alternative<T>(m_data);
}
bool is_error() const
{
return std::holds_alternative<Error>(m_data);
}
explicit operator bool() const
{
return is_ok();
}
T &value() &
{
return std::get<T>(m_data);
}
const T &value() const &
{
return std::get<T>(m_data);
}
T &&value() &&
{
return std::get<T>(std::move(m_data));
}
const Error &error() const
{
return std::get<Error>(m_data);
}
T value_or(T default_value) const
{
return is_ok() ? std::get<T>(m_data) : std::move(default_value);
}
std::optional<T> ok() const
{
if (is_ok())
return std::get<T>(m_data);
return std::nullopt;
}
std::optional<Error> err() const
{
if (is_error())
return std::get<Error>(m_data);
return std::nullopt;
}
template <typename F> auto map(F &&func) -> Result<decltype(func(std::declval<T>()))>
{
using U = decltype(func(std::declval<T>()));
if (is_ok())
return Result<U>(func(std::get<T>(m_data)));
return Result<U>(std::get<Error>(m_data));
}
template <typename F> auto and_then(F &&func) -> decltype(func(std::declval<T>()))
{
if (is_ok())
return func(std::get<T>(m_data));
using ResultType = decltype(func(std::declval<T>()));
return ResultType(std::get<Error>(m_data));
}
};
template <> class [[nodiscard]] Result<void>
{
private:
std::optional<Error> m_error;
public:
Result() : m_error(std::nullopt)
{
}
Result(Error error) : m_error(std::move(error))
{
}
bool is_ok() const
{
return !m_error.has_value();
}
bool is_error() const
{
return m_error.has_value();
}
explicit operator bool() const
{
return is_ok();
}
const Error &error() const
{
return *m_error;
}
std::optional<Error> err() const
{
return m_error;
}
};
template <typename T> Result<T> Ok(T value)
{
return Result<T>(std::move(value));
}
inline Result<void> Ok()
{
return Result<void>();
}
template <typename T> Result<T> Err(Error error)
{
return Result<T>(std::move(error));
}
template <typename T> Result<T> Err(error_code code)
{
return Result<T>(Error(code));
}
template <typename T> Result<T> Err(error_code code, std::string message)
{
return Result<T>(Error(code, std::move(message)));
}
template <typename T> Result<T> Err(error_code code, std::string message, std::string context)
{
return Result<T>(Error(code, std::move(message), std::move(context)));
}
} // namespace clrsync::core
#endif // CLRSYNC_CORE_ERROR_HPP

56
src/core/common/utils.cpp Normal file
View File

@@ -0,0 +1,56 @@
#include "utils.hpp"
#include <filesystem>
#include <iostream>
namespace clrsync::core
{
void print_color_keys()
{
for (const auto &key : clrsync::core::COLOR_KEYS)
{
std::cout << key << std::endl;
}
}
std::string get_default_config_path()
{
const char *env_path = std::getenv("CLRSYNC_CONFIG_PATH");
if (env_path && env_path[0] != '\0')
return normalize_path(env_path).string();
std::filesystem::path home = normalize_path("~");
std::filesystem::path config_path = home / ".config" / "clrsync" / "config.toml";
return config_path.string();
}
std::string expand_user(const std::string &path)
{
if (path.empty() || path[0] != '~')
return path;
if (path.length() == 1 || path[1] == '/' || path[1] == '\\')
{
#ifdef _WIN32
const char *home = std::getenv("USERPROFILE");
#else
const char *home = std::getenv("HOME");
#endif
if (!home)
return path;
if (path.length() == 1)
return std::string(home);
return std::string(home) + path.substr(1);
}
return path;
}
std::filesystem::path normalize_path(const std::string &path)
{
std::string expanded = expand_user(path);
std::filesystem::path fs_path(expanded);
return fs_path.lexically_normal();
}
} // namespace clrsync::core

View File

@@ -1,14 +1,16 @@
#ifndef CLRSYNC_CORE_UTILS_HPP #ifndef CLRSYNC_CORE_UTILS_HPP
#define CLRSYNC_CORE_UTILS_HPP #define CLRSYNC_CORE_UTILS_HPP
#include <filesystem>
#include <string> #include <string>
#include <core/palette/color_keys.hpp> #include "core/palette/color_keys.hpp"
namespace clrsync::core namespace clrsync::core
{ {
void print_color_keys(); void print_color_keys();
std::string get_default_config_path(); std::string get_default_config_path();
std::string expand_user(const std::string &path); std::string expand_user(const std::string &path);
std::filesystem::path normalize_path(const std::string &path);
} // namespace clrsync::core } // namespace clrsync::core
#endif // CLRSYNC_CORE_UTILS_HPP #endif // CLRSYNC_CORE_UTILS_HPP

View File

@@ -0,0 +1,9 @@
#include "core/common/version.hpp"
namespace clrsync::core
{
const std::string version_string()
{
return GIT_SEMVER;
}
} // namespace clrsync::core

View File

@@ -1,15 +1,12 @@
#ifndef CLRSYNC_CORE_VERSION_HPP #ifndef CLRSYNC_CORE_VERSION_HPP
#define CLRSYNC_CORE_VERSION_HPP #define CLRSYNC_CORE_VERSION_HPP
#include <cstdint>
#include <string> #include <string>
namespace clrsync::core namespace clrsync::core
{ {
constexpr uint8_t VERSION_MAJOR = 0; const std::string GIT_SEMVER = "0.1.6+git.gdff3e91";
constexpr uint8_t VERSION_MINOR = 1;
constexpr uint8_t VERSION_PATCH = 3;
const std::string version_string(); const std::string version_string();
} // namespace clrsync::core } // namespace clrsync::core

View File

@@ -1,15 +1,12 @@
#ifndef CLRSYNC_CORE_VERSION_HPP #ifndef CLRSYNC_CORE_VERSION_HPP
#define CLRSYNC_CORE_VERSION_HPP #define CLRSYNC_CORE_VERSION_HPP
#include <cstdint>
#include <string> #include <string>
namespace clrsync::core namespace clrsync::core
{ {
constexpr uint8_t VERSION_MAJOR = @PROJECT_VERSION_MAJOR@; const std::string GIT_SEMVER = "@SEMVER@";
constexpr uint8_t VERSION_MINOR = @PROJECT_VERSION_MINOR@;
constexpr uint8_t VERSION_PATCH = @PROJECT_VERSION_PATCH@;
const std::string version_string(); const std::string version_string();
} // namespace clrsync::core } // namespace clrsync::core

View File

@@ -1,13 +1,16 @@
#include "config.hpp" #include "config.hpp"
#include "core/utils.hpp" #include "core/common/error.hpp"
#include "core/common/utils.hpp"
#include "core/io/toml_file.hpp"
#include <core/palette/color.hpp> #include "core/palette/color.hpp"
#include <filesystem> #include <filesystem>
#include <stdexcept> #include <fstream>
#ifdef _WIN32 #ifdef _WIN32
#include "windows.h" #include "windows.h"
#endif #endif
#include <iostream>
namespace clrsync::core namespace clrsync::core
{ {
@@ -17,24 +20,80 @@ config &config::instance()
return inst; return inst;
} }
void config::initialize(std::unique_ptr<clrsync::core::io::file> file) Result<void> config::initialize(std::unique_ptr<clrsync::core::io::file> file)
{ {
copy_default_configs(); copy_default_configs();
m_file = std::move(file); m_file = std::move(file);
if (m_file) if (!m_file)
if (!m_file->parse()) return Err<void>(error_code::config_missing, "Config file is missing");
throw std::runtime_error{"Could not parse config file"};
auto parse_result = m_file->parse();
if (!parse_result)
return Err<void>(error_code::config_invalid, parse_result.error().message,
parse_result.error().context);
std::filesystem::path config_path = get_user_config_dir() / "config.toml";
std::filesystem::path temp_config_path = get_user_config_dir() / "config-temp.toml";
if (std::filesystem::exists(config_path))
{
std::error_code ec;
auto perms = std::filesystem::status(config_path, ec).permissions();
if (ec || (perms & std::filesystem::perms::owner_write) == std::filesystem::perms::none)
{
m_temp_config_path = temp_config_path.string();
if (std::filesystem::exists(temp_config_path))
{
try
{
auto temp_conf = std::make_unique<clrsync::core::io::toml_file>(temp_config_path.string());
auto temp_parse = temp_conf->parse();
if (temp_parse)
{
m_temp_file = std::move(temp_conf);
}
}
catch (const std::exception &e)
{
std::cerr << "Warning: Failed to load temp config: " << e.what() << std::endl;
}
}
}
}
return Ok();
} }
std::filesystem::path config::get_user_config_dir() std::filesystem::path config::get_user_config_dir()
{ {
auto home = expand_user("~"); std::filesystem::path home = normalize_path("~");
#ifdef _WIN32 return home / ".config" / "clrsync";
return home + "\\.config\\clrsync"; }
#else std::filesystem::path config::get_user_state_dir()
return home + "/.config/clrsync"; {
#endif std::filesystem::path home = normalize_path("~");
return home / ".local" / "state" / "clrsync";
}
std::filesystem::path config::get_writable_config_path()
{
std::filesystem::path config_path = get_user_config_dir() / "config.toml";
if (std::filesystem::exists(config_path))
{
std::error_code ec;
auto perms = std::filesystem::status(config_path, ec).permissions();
if (ec || (perms & std::filesystem::perms::owner_write) == std::filesystem::perms::none)
{
return get_user_config_dir() / "config-temp.toml";
}
}
return config_path;
} }
std::filesystem::path config::get_data_dir() std::filesystem::path config::get_data_dir()
@@ -56,37 +115,126 @@ std::filesystem::path config::get_data_dir()
#endif #endif
} }
void config::copy_file(const std::filesystem::path &src, const std::filesystem::path &dst)
{
if (std::filesystem::exists(dst))
return;
if (!std::filesystem::exists(src))
return;
std::ifstream in(src, std::ios::binary);
std::ofstream out(dst, std::ios::binary);
if (!in || !out)
return;
out << in.rdbuf();
}
void config::copy_dir(const std::filesystem::path &src, const std::filesystem::path &dst)
{
if (!std::filesystem::exists(src))
return;
for (auto const &entry : std::filesystem::recursive_directory_iterator(src))
{
auto rel = std::filesystem::relative(entry.path(), src);
auto out = dst / rel;
if (entry.is_directory())
{
std::filesystem::create_directories(out);
}
else if (entry.is_regular_file())
{
copy_file(entry.path(), out);
}
}
}
void config::copy_default_configs() void config::copy_default_configs()
{ {
std::filesystem::path user_config = get_user_config_dir(); std::filesystem::path user_dir = get_user_config_dir();
std::filesystem::path default_dir = get_data_dir(); std::filesystem::path system_dir = get_data_dir();
if (!std::filesystem::exists(user_config)) std::filesystem::create_directories(user_dir);
{
std::filesystem::create_directories(user_config);
std::filesystem::copy(default_dir / "config.toml", user_config / "config.toml"); if (system_dir.empty())
std::filesystem::copy(default_dir / "templates", user_config / "templates",
std::filesystem::copy_options::recursive);
std::filesystem::copy(default_dir / "palettes", user_config / "palettes",
std::filesystem::copy_options::recursive);
return; return;
}
if (!std::filesystem::exists(user_config / "config.toml"))
{ {
std::filesystem::copy(default_dir / "config.toml", user_config / "config.toml"); auto src = system_dir / "config.toml";
auto dst = user_dir / "config.toml";
if (!std::filesystem::exists(dst))
copy_file(src, dst);
} }
{
auto src = system_dir / "templates";
auto dst = user_dir / "templates";
if (!std::filesystem::exists(dst))
std::filesystem::create_directories(dst);
copy_dir(src, dst);
}
{
auto src = system_dir / "palettes";
auto dst = user_dir / "palettes";
if (!std::filesystem::exists(dst))
std::filesystem::create_directories(dst);
copy_dir(src, dst);
}
}
Result<void> config::save_config_value(const std::string &section, const std::string &key, const value_type &value)
{
if (!m_temp_config_path.empty())
{
if (!m_temp_file)
{
m_temp_file = std::make_unique<clrsync::core::io::toml_file>(m_temp_config_path);
m_temp_file->parse();
}
m_temp_file->set_value(section, key, value);
return m_temp_file->save_file();
}
m_file->set_value(section, key, value);
return m_file->save_file();
} }
const std::string &config::palettes_path() const std::string &config::palettes_path()
{ {
if (m_palettes_dir.empty() && m_file) if (m_palettes_dir.empty() && m_file)
{
if (m_temp_file)
{
auto temp_value = m_temp_file->get_string_value("general", "palettes_path");
if (!temp_value.empty())
{
m_palettes_dir = temp_value;
return m_palettes_dir;
}
}
m_palettes_dir = m_file->get_string_value("general", "palettes_path"); m_palettes_dir = m_file->get_string_value("general", "palettes_path");
}
return m_palettes_dir; return m_palettes_dir;
} }
const std::string config::default_theme() const const std::string config::default_theme() const
{ {
if (m_temp_file)
{
auto temp_value = m_temp_file->get_string_value("general", "default_theme");
if (!temp_value.empty())
return temp_value;
}
if (m_file) if (m_file)
return m_file->get_string_value("general", "default_theme"); return m_file->get_string_value("general", "default_theme");
return {}; return {};
@@ -94,6 +242,12 @@ const std::string config::default_theme() const
const std::string config::font() const const std::string config::font() const
{ {
if (m_temp_file)
{
auto temp_value = m_temp_file->get_string_value("general", "font");
if (!temp_value.empty())
return temp_value;
}
if (m_file) if (m_file)
return m_file->get_string_value("general", "font"); return m_file->get_string_value("general", "font");
return {}; return {};
@@ -101,55 +255,106 @@ const std::string config::font() const
const uint32_t config::font_size() const const uint32_t config::font_size() const
{ {
if (m_temp_file)
{
auto temp_value = m_temp_file->get_uint_value("general", "font_size");
if (temp_value != 0)
return temp_value;
}
if (m_file) if (m_file)
return m_file->get_uint_value("general", "font_size"); return m_file->get_uint_value("general", "font_size");
return 14; return 14;
} }
void config::set_default_theme(const std::string &theme) Result<void> config::set_default_theme(const std::string &theme)
{ {
if (m_file) if (!m_file)
{ return Err<void>(error_code::config_missing, "Configuration not initialized");
m_file->set_value("general", "default_theme", theme);
m_file->save_file(); return save_config_value("general", "default_theme", theme);
}
} }
void config::set_palettes_path(const std::string &path) Result<void> config::set_palettes_path(const std::string &path)
{ {
if (m_file) if (!m_file)
{ return Err<void>(error_code::config_missing, "Configuration not initialized");
m_file->set_value("general", "palettes_path", path);
m_file->save_file(); return save_config_value("general", "palettes_path", path);
}
} }
void config::set_font(const std::string &font) Result<void> config::set_font(const std::string &font)
{ {
if (m_file) if (!m_file)
{ return Err<void>(error_code::config_missing, "Configuration not initialized");
m_file->set_value("general", "font", font);
m_file->save_file(); return save_config_value("general", "font", font);
} }
} Result<void> config::set_font_size(int font_size)
void config::set_font_size(int font_size)
{ {
if (m_file) if (!m_file)
{ return Err<void>(error_code::config_missing, "Configuration not initialized");
m_file->set_value("general", "font_size", font_size);
m_file->save_file(); return save_config_value("general", "font_size", static_cast<uint32_t>(font_size));
}
} }
void config::update_template(const std::string &key, Result<void> config::update_template(const std::string &key,
const clrsync::core::theme_template &theme_template) const clrsync::core::theme_template &theme_template)
{ {
if (!m_file)
return Err<void>(error_code::config_missing, "Configuration not initialized");
m_themes[key] = theme_template; m_themes[key] = theme_template;
m_file->set_value("templates." + key, "input_path", theme_template.template_path());
m_file->set_value("templates." + key, "output_path", theme_template.output_path()); auto result1 = save_config_value("templates." + key, "input_path", theme_template.template_path());
m_file->set_value("templates." + key, "enabled", theme_template.enabled()); if (!result1) return result1;
m_file->set_value("templates." + key, "reload_cmd", theme_template.reload_command());
m_file->save_file(); auto result2 = save_config_value("templates." + key, "output_path", theme_template.output_path());
if (!result2) return result2;
auto result3 = save_config_value("templates." + key, "enabled", theme_template.enabled());
if (!result3) return result3;
return save_config_value("templates." + key, "reload_cmd", theme_template.reload_command());
}
Result<void> config::remove_template(const std::string &key)
{
if (!m_file)
return Err<void>(error_code::config_missing, "Configuration not initialized");
auto it = m_themes.find(key);
if (it == m_themes.end())
return Err<void>(error_code::template_not_found, "Template not found", key);
std::filesystem::path template_file = it->second.template_path();
if (std::filesystem::exists(template_file))
{
try
{
std::filesystem::remove(template_file);
}
catch (const std::exception &e)
{
return Err<void>(error_code::file_write_failed, "Failed to delete template file",
e.what());
}
}
m_themes.erase(it);
if (!m_temp_config_path.empty())
{
if (!m_temp_file)
{
m_temp_file = std::make_unique<clrsync::core::io::toml_file>(m_temp_config_path);
m_temp_file->parse();
}
m_temp_file->remove_section("templates." + key);
return m_temp_file->save_file();
}
m_file->remove_section("templates." + key);
return m_file->save_file();
} }
const std::unordered_map<std::string, clrsync::core::theme_template> config::templates() const std::unordered_map<std::string, clrsync::core::theme_template> config::templates()
@@ -172,21 +377,23 @@ const std::unordered_map<std::string, clrsync::core::theme_template> config::tem
theme.set_enabled(false); theme.set_enabled(false);
} }
theme.set_reload_command(std::get<std::string>(current["reload_cmd"])); theme.set_reload_command(std::get<std::string>(current["reload_cmd"]));
theme.load_template(); (void)theme.load_template();
m_themes.insert({theme.name(), theme}); m_themes.insert({theme.name(), theme});
} }
} }
return m_themes; return m_themes;
} }
const clrsync::core::theme_template &config::template_by_name(const std::string &name) const Result<const clrsync::core::theme_template *> config::template_by_name(
const std::string &name) const
{ {
auto it = m_themes.find(name); auto it = m_themes.find(name);
if (it != m_themes.end()) if (it != m_themes.end())
{ {
return it->second; return Ok(&it->second);
} }
throw std::runtime_error("Template not found: " + name); return Err<const clrsync::core::theme_template *>(error_code::template_not_found,
"Template not found", name);
} }
} // namespace clrsync::core } // namespace clrsync::core

View File

@@ -1,8 +1,9 @@
#ifndef CLRSYNC_CORE_CONFIG_HPP #ifndef CLRSYNC_CORE_CONFIG_HPP
#define CLRSYNC_CORE_CONFIG_HPP #define CLRSYNC_CORE_CONFIG_HPP
#include <core/io/file.hpp> #include "core/common/error.hpp"
#include <core/theme/theme_template.hpp> #include "core/io/file.hpp"
#include "core/theme/theme_template.hpp"
#include <filesystem> #include <filesystem>
#include <memory> #include <memory>
#include <string> #include <string>
@@ -14,24 +15,26 @@ class config
public: public:
static config &instance(); static config &instance();
void initialize(std::unique_ptr<clrsync::core::io::file> file); Result<void> initialize(std::unique_ptr<clrsync::core::io::file> file);
const std::string font() const; const std::string font() const;
const uint32_t font_size() const; const uint32_t font_size() const;
const std::string &palettes_path(); const std::string &palettes_path();
const std::string default_theme() const; const std::string default_theme() const;
const std::unordered_map<std::string, clrsync::core::theme_template> templates(); const std::unordered_map<std::string, clrsync::core::theme_template> templates();
const clrsync::core::theme_template &template_by_name(const std::string &name) const; Result<const clrsync::core::theme_template *> template_by_name(const std::string &name) const;
std::filesystem::path get_user_config_dir(); std::filesystem::path get_user_config_dir();
std::filesystem::path get_user_state_dir();
std::filesystem::path get_writable_config_path();
Result<void> set_default_theme(const std::string &theme);
Result<void> set_palettes_path(const std::string &path);
Result<void> set_font(const std::string &font);
Result<void> set_font_size(int font_size);
void set_default_theme(const std::string &theme); Result<void> update_template(const std::string &key,
void set_palettes_path(const std::string &path);
void set_font(const std::string &font);
void set_font_size(int font_size);
void update_template(const std::string &key,
const clrsync::core::theme_template &theme_template); const clrsync::core::theme_template &theme_template);
Result<void> remove_template(const std::string &key);
static std::filesystem::path get_data_dir(); static std::filesystem::path get_data_dir();
private: private:
@@ -41,7 +44,12 @@ class config
std::string m_palettes_dir{}; std::string m_palettes_dir{};
std::unique_ptr<io::file> m_file; std::unique_ptr<io::file> m_file;
std::unique_ptr<io::file> m_temp_file;
std::string m_temp_config_path;
std::unordered_map<std::string, theme_template> m_themes{}; std::unordered_map<std::string, theme_template> m_themes{};
Result<void> save_config_value(const std::string &section, const std::string &key, const value_type &value);
static void copy_file(const std::filesystem::path &src, const std::filesystem::path &dst);
static void copy_dir(const std::filesystem::path &src, const std::filesystem::path &dst);
void copy_default_configs(); void copy_default_configs();
}; };
} // namespace clrsync::core } // namespace clrsync::core

View File

@@ -1,5 +1,6 @@
#ifndef CLRSYNC_CORE_IO_FILE_HPP #ifndef CLRSYNC_CORE_IO_FILE_HPP
#define CLRSYNC_CORE_IO_FILE_HPP #define CLRSYNC_CORE_IO_FILE_HPP
#include "core/common/error.hpp"
#include <cstdint> #include <cstdint>
#include <map> #include <map>
#include <string> #include <string>
@@ -14,7 +15,11 @@ class file
public: public:
file() = default; file() = default;
file(std::string path) {}; file(std::string path) {};
virtual bool parse() { return false; }; virtual ~file() = default;
virtual Result<void> parse()
{
return Ok();
};
virtual const std::string get_string_value(const std::string &section, virtual const std::string get_string_value(const std::string &section,
const std::string &key) const const std::string &key) const
{ {
@@ -39,7 +44,11 @@ class file
} }
virtual void insert_or_update_value(const std::string &section, const std::string &key, virtual void insert_or_update_value(const std::string &section, const std::string &key,
const value_type &value) {}; const value_type &value) {};
virtual void save_file() {}; virtual void remove_section(const std::string &section) {};
virtual Result<void> save_file()
{
return Ok();
};
}; };
} // namespace clrsync::core::io } // namespace clrsync::core::io
#endif #endif

View File

@@ -1,5 +1,5 @@
#include "toml_file.hpp" #include "core/io/toml_file.hpp"
#include "core/utils.hpp" #include "core/common/utils.hpp"
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <vector> #include <vector>
@@ -8,15 +8,16 @@ namespace clrsync::core::io
{ {
toml_file::toml_file(std::string path) toml_file::toml_file(std::string path)
{ {
m_path = expand_user(path); m_path = normalize_path(path).string();
} }
bool toml_file::parse() Result<void> toml_file::parse()
{ {
if (!std::filesystem::exists(m_path)) if (!std::filesystem::exists(m_path))
return false; return Err<void>(error_code::file_not_found, "File does not exist", m_path);
m_file = toml::parse_file(m_path); m_file = toml::parse_file(m_path);
return true; return Ok();
} }
const std::string toml_file::get_string_value(const std::string &section, const std::string toml_file::get_string_value(const std::string &section,
@@ -63,7 +64,7 @@ std::map<std::string, value_type> toml_file::get_table(const std::string &sectio
else if (auto d = val.value<double>()) else if (auto d = val.value<double>())
result[std::string(p.first.str())] = static_cast<uint32_t>(*d); result[std::string(p.first.str())] = static_cast<uint32_t>(*d);
else else
result[std::string(p.first.str())] = {}; // fallback for unsupported types result[std::string(p.first.str())] = {};
} }
return result; return result;
@@ -91,11 +92,45 @@ void toml_file::insert_or_update_value(const std::string &section, const std::st
std::visit([&](auto &&v) { tbl->insert_or_assign(key, v); }, value); std::visit([&](auto &&v) { tbl->insert_or_assign(key, v); }, value);
} }
void toml_file::save_file() void toml_file::remove_section(const std::string &section)
{
toml::table *tbl = m_file.as_table();
auto parts = split(section, '.');
if (parts.empty())
return;
for (size_t i = 0; i < parts.size() - 1; ++i)
{
auto *sub = (*tbl)[parts[i]].as_table();
if (!sub)
return;
tbl = sub;
}
tbl->erase(parts.back());
}
Result<void> toml_file::save_file()
{
try
{ {
std::filesystem::create_directories(std::filesystem::path(m_path).parent_path()); std::filesystem::create_directories(std::filesystem::path(m_path).parent_path());
}
catch (const std::exception &e)
{
return Err<void>(error_code::dir_create_failed, e.what(), m_path);
}
std::ofstream stream(m_path, std::ios::binary); std::ofstream stream(m_path, std::ios::binary);
if (!stream)
return Err<void>(error_code::file_write_failed, "Failed to open file for writing", m_path);
stream << m_file; stream << m_file;
if (!stream)
return Err<void>(error_code::file_write_failed, "Failed to write to file", m_path);
return Ok();
} }
std::vector<std::string> toml_file::split(const std::string &s, char delim) const std::vector<std::string> toml_file::split(const std::string &s, char delim) const

View File

@@ -1,6 +1,7 @@
#ifndef CLRSYNC_CORE_IO_TOML_FILE_HPP #ifndef CLRSYNC_CORE_IO_TOML_FILE_HPP
#define CLRSYNC_CORE_IO_TOML_FILE_HPP #define CLRSYNC_CORE_IO_TOML_FILE_HPP
#include <core/io/file.hpp> #include "core/common/error.hpp"
#include "core/io/file.hpp"
#include <string> #include <string>
#include <toml/toml.hpp> #include <toml/toml.hpp>
@@ -10,7 +11,7 @@ class toml_file : public file
{ {
public: public:
explicit toml_file(std::string path); explicit toml_file(std::string path);
bool parse() override; Result<void> parse() override;
const std::string get_string_value(const std::string &section, const std::string get_string_value(const std::string &section,
const std::string &key) const override; const std::string &key) const override;
uint32_t get_uint_value(const std::string &section, const std::string &key) const override; uint32_t get_uint_value(const std::string &section, const std::string &key) const override;
@@ -18,7 +19,8 @@ class toml_file : public file
std::map<std::string, value_type> get_table(const std::string &section_path) const override; std::map<std::string, value_type> get_table(const std::string &section_path) const override;
void insert_or_update_value(const std::string &section, const std::string &key, void insert_or_update_value(const std::string &section, const std::string &key,
const value_type &value) override; const value_type &value) override;
void save_file() override; void remove_section(const std::string &section) override;
Result<void> save_file() override;
private: private:
toml::parse_result m_file{}; toml::parse_result m_file{};

View File

@@ -3,7 +3,6 @@
#include <cstdio> #include <cstdio>
#include <format> #include <format>
#include <stdexcept> #include <stdexcept>
#include <unordered_map>
namespace clrsync::core namespace clrsync::core
{ {
@@ -112,14 +111,17 @@ void color::from_hex_string(const std::string &str)
if (str.empty() || str[0] != '#') if (str.empty() || str[0] != '#')
throw std::invalid_argument("Invalid hex color format"); throw std::invalid_argument("Invalid hex color format");
if (str.size() == 7) { if (str.size() == 7)
{
uint32_t rgb = static_cast<uint32_t>(std::stoul(str.substr(1), nullptr, 16)); uint32_t rgb = static_cast<uint32_t>(std::stoul(str.substr(1), nullptr, 16));
m_hex = (rgb << 8) | 0xFF; m_hex = (rgb << 8) | 0xFF;
} }
else if (str.size() == 9) { else if (str.size() == 9)
{
m_hex = static_cast<uint32_t>(std::stoul(str.substr(1), nullptr, 16)); m_hex = static_cast<uint32_t>(std::stoul(str.substr(1), nullptr, 16));
} }
else { else
{
throw std::invalid_argument("Invalid hex color format"); throw std::invalid_argument("Invalid hex color format");
} }
} }
@@ -145,23 +147,31 @@ std::string color::format(const std::string& field) const
auto hslv = to_hsl(); auto hslv = to_hsl();
auto hslav = to_hsla(); auto hslav = to_hsla();
if (field == "hex") return to_hex_string(); if (field == "hex")
if (field == "hex_stripped") { return to_hex_string();
if (field == "hex_stripped")
{
auto s = to_hex_string(); auto s = to_hex_string();
return s.substr(1); return s.substr(1);
} }
if (field == "hexa") return to_hex_string_with_alpha(); if (field == "hexa")
if (field == "hexa_stripped") { return to_hex_string_with_alpha();
if (field == "hexa_stripped")
{
auto s = to_hex_string_with_alpha(); auto s = to_hex_string_with_alpha();
return s.substr(1); return s.substr(1);
} }
if (field == "r") return std::to_string(rgb.r); if (field == "r")
if (field == "g") return std::to_string(rgb.g); return std::to_string(rgb.r);
if (field == "b") return std::to_string(rgb.b); if (field == "g")
return std::to_string(rgb.g);
if (field == "b")
return std::to_string(rgb.b);
if (field == "a") { if (field == "a")
{
float af = rgba.a / 255.0f; float af = rgba.a / 255.0f;
return std::format("{:.2f}", af); return std::format("{:.2f}", af);
} }
@@ -170,9 +180,7 @@ std::string color::format(const std::string& field) const
return std::format("rgb({},{},{})", rgb.r, rgb.g, rgb.b); return std::format("rgb({},{},{})", rgb.r, rgb.g, rgb.b);
if (field == "rgba") if (field == "rgba")
return std::format("rgba({},{},{},{:.2f})", return std::format("rgba({},{},{},{:.2f})", rgba.r, rgba.g, rgba.b, rgba.a / 255.0f);
rgba.r, rgba.g, rgba.b,
rgba.a / 255.0f);
if (field == "h") if (field == "h")
return std::format("{:.0f}", hslv.h); return std::format("{:.0f}", hslv.h);
@@ -186,12 +194,10 @@ std::string color::format(const std::string& field) const
return std::format("{:.2f}", hslav.a); return std::format("{:.2f}", hslav.a);
if (field == "hsl") if (field == "hsl")
return std::format("hsl({:.0f},{:.2f},{:.2f})", return std::format("hsl({:.0f},{:.2f},{:.2f})", hslv.h, hslv.s, hslv.l);
hslv.h, hslv.s, hslv.l);
if (field == "hsla") if (field == "hsla")
return std::format("hsla({:.0f},{:.2f},{:.2f},{:.2f})", return std::format("hsla({:.0f},{:.2f},{:.2f},{:.2f})", hslav.h, hslav.s, hslav.l, hslav.a);
hslav.h, hslav.s, hslav.l, hslav.a);
throw std::runtime_error("Unknown color format: " + field); throw std::runtime_error("Unknown color format: " + field);
} }

View File

@@ -1,7 +1,10 @@
#ifndef CLRSYNC_CORE_PALETTE_COLOR_KEYS_HPP #ifndef CLRSYNC_CORE_PALETTE_COLOR_KEYS_HPP
#define CLRSYNC_CORE_PALETTE_COLOR_KEYS_HPP #define CLRSYNC_CORE_PALETTE_COLOR_KEYS_HPP
#include <cstddef> #include <cstddef>
#include <cstdint>
#include <iterator> #include <iterator>
#include <string>
#include <unordered_map>
namespace clrsync::core namespace clrsync::core
{ {
@@ -14,7 +17,7 @@ constexpr const char* COLOR_KEYS[] = {
"on_surface", "on_surface",
"surface_variant", "surface_variant",
"on_surface_varuant", "on_surface_variant",
"border_focused", "border_focused",
"border", "border",
@@ -72,5 +75,67 @@ constexpr const char* COLOR_KEYS[] = {
}; };
constexpr size_t NUM_COLOR_KEYS = std::size(COLOR_KEYS); constexpr size_t NUM_COLOR_KEYS = std::size(COLOR_KEYS);
inline const std::unordered_map<std::string, uint32_t> DEFAULT_COLORS = {
{"background", 0x111111ff},
{"on_background", 0xd4d4d4ff},
{"surface", 0x111111ff},
{"on_surface", 0xd4d4d4ff},
{"surface_variant", 0x191919ff},
{"on_surface_variant", 0xd4d4d4ff},
{"border_focused", 0x2e2e2eff},
{"border", 0x242424ff},
{"foreground", 0xd2d2d2ff},
{"cursor", 0xd2d2d2ff},
{"accent", 0x9a8652ff},
{"success", 0x668a51ff},
{"info", 0x3a898cff},
{"warning", 0xb47837ff},
{"error", 0xaa4e4aff},
{"on_success", 0xd2d2d2ff},
{"on_info", 0xd2d2d2ff},
{"on_warning", 0xd2d2d2ff},
{"on_error", 0xd2d2d2ff},
{"editor_background", 0x111111ff},
{"editor_command", 0x3a898cff},
{"editor_comment", 0x849899ff},
{"editor_disabled", 0x849899ff},
{"editor_emphasis", 0xa9dc86ff},
{"editor_error", 0xaa4e4aff},
{"editor_inactive", 0x849899ff},
{"editor_line_number", 0x849899ff},
{"editor_link", 0xb0779eff},
{"editor_main", 0xd2d2d2ff},
{"editor_selected", 0x242424ff},
{"editor_selection_inactive", 0x1d1c1cff},
{"editor_string", 0x9a8652ff},
{"editor_success", 0x668a51ff},
{"editor_warning", 0xb47837ff},
{"base00", 0x111111ff},
{"base01", 0x668a51ff},
{"base02", 0x9a8652ff},
{"base03", 0xb47837ff},
{"base04", 0x9a5552ff},
{"base05", 0xaa477bff},
{"base06", 0x3a898cff},
{"base07", 0xb5b5b5ff},
{"base08", 0xaa4e4aff},
{"base09", 0xa9dc86ff},
{"base0A", 0xb6ab82ff},
{"base0B", 0xc5916bff},
{"base0C", 0xac7676ff},
{"base0D", 0xb0779eff},
{"base0E", 0x849899ff},
{"base0F", 0xd2d2d2ff},
};
} // namespace clrsync::core } // namespace clrsync::core
#endif #endif

View File

@@ -4,7 +4,8 @@
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <core/palette/color.hpp> #include "core/palette/color.hpp"
#include "core/palette/color_keys.hpp"
namespace clrsync::core namespace clrsync::core
{ {
@@ -37,9 +38,16 @@ class palette
{ {
return it->second; return it->second;
} }
static color default_color{}; auto default_it = DEFAULT_COLORS.find(key);
if (default_it != DEFAULT_COLORS.end())
{
static color default_color;
default_color.set(default_it->second);
return default_color; return default_color;
} }
static color empty_color{};
return empty_color;
}
const std::unordered_map<std::string, color> &colors() const const std::unordered_map<std::string, color> &colors() const
{ {

View File

@@ -5,9 +5,9 @@
#include <cstdint> #include <cstdint>
#include <string> #include <string>
#include <core/io/file.hpp> #include "core/io/file.hpp"
#include <core/palette/color_keys.hpp> #include "core/palette/color_keys.hpp"
#include <core/palette/palette.hpp> #include "core/palette/palette.hpp"
#include <memory> #include <memory>
@@ -26,14 +26,26 @@ template <typename FileType> class palette_file
if (!m_file->parse()) if (!m_file->parse())
return false; return false;
m_palette.set_name(m_file->get_string_value("general", "name")); m_palette.set_name(m_file->get_string_value("general", "name"));
for (const auto &color_key : COLOR_KEYS)
{
auto it = DEFAULT_COLORS.find(color_key);
if (it != DEFAULT_COLORS.end())
{
m_palette.set_color(color_key, core::color(it->second));
}
}
for (const auto &color_key : COLOR_KEYS) for (const auto &color_key : COLOR_KEYS)
{ {
auto color_str = m_file->get_string_value("colors", color_key); auto color_str = m_file->get_string_value("colors", color_key);
core::color color{0x000000FF};
if (!color_str.empty()) if (!color_str.empty())
{
core::color color;
color.from_hex_string(color_str); color.from_hex_string(color_str);
m_palette.set_color(color_key, color); m_palette.set_color(color_key, color);
} }
}
return true; return true;
} }
core::palette palette() const core::palette palette() const
@@ -52,7 +64,7 @@ template <typename FileType> class palette_file
} }
void save() void save()
{ {
m_file->save_file(); (void)m_file->save_file();
} }
private: private:

View File

@@ -1,14 +1,13 @@
#ifndef CLRSYNC_CORE_PALETTE_PALETTE_MANAGER_HPP #ifndef CLRSYNC_CORE_PALETTE_PALETTE_MANAGER_HPP
#define CLRSYNC_CORE_PALETTE_PALETTE_MANAGER_HPP #define CLRSYNC_CORE_PALETTE_PALETTE_MANAGER_HPP
#include "core/utils.hpp" #include "core/common/utils.hpp"
#include <iostream>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <core/config/config.hpp> #include "core/config/config.hpp"
#include <core/palette/palette.hpp> #include "core/palette/palette.hpp"
#include <core/palette/palette_file.hpp> #include "core/palette/palette_file.hpp"
#include <filesystem> #include <filesystem>
namespace clrsync::core namespace clrsync::core
@@ -19,12 +18,9 @@ template <typename FileType> class palette_manager
palette_manager() = default; palette_manager() = default;
void load_palettes_from_directory(const std::string &directory_path) void load_palettes_from_directory(const std::string &directory_path)
{ {
auto directory_path_expanded = expand_user(directory_path); std::filesystem::path directory_path_expanded = normalize_path(directory_path);
if (!std::filesystem::exists(directory_path_expanded)) if (!std::filesystem::exists(directory_path_expanded))
{
std::cerr << "Palettes directory does not exist\n" ;
return; return;
}
for (const auto &entry : std::filesystem::directory_iterator(directory_path_expanded)) for (const auto &entry : std::filesystem::directory_iterator(directory_path_expanded))
{ {
if (entry.is_regular_file()) if (entry.is_regular_file())
@@ -37,8 +33,9 @@ template <typename FileType> class palette_manager
} }
void save_palette_to_file(const palette &pal, const std::string &directory_path) const void save_palette_to_file(const palette &pal, const std::string &directory_path) const
{ {
std::string file_path = directory_path + "/" + pal.name() + ".toml"; std::filesystem::path dir_path = normalize_path(directory_path);
palette_file<FileType> pal_file(file_path); std::filesystem::path file_path = dir_path / (pal.name() + ".toml");
palette_file<FileType> pal_file(file_path.string());
pal_file.save_palette(pal); pal_file.save_palette(pal);
} }

View File

@@ -1,17 +1,15 @@
#ifndef CLRSYNC_CORE_THEME_TEMPLATE_MANAGER_HPP #ifndef CLRSYNC_CORE_THEME_TEMPLATE_MANAGER_HPP
#define CLRSYNC_CORE_THEME_TEMPLATE_MANAGER_HPP #define CLRSYNC_CORE_THEME_TEMPLATE_MANAGER_HPP
#include <core/config/config.hpp> #include "core/config/config.hpp"
#include <core/theme/theme_template.hpp> #include "core/theme/theme_template.hpp"
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
namespace clrsync::core namespace clrsync::core
{ {
template <typename FileType> template <typename FileType> class template_manager
class template_manager
{ {
public: public:
template_manager() = default; template_manager() = default;

View File

@@ -1,8 +1,10 @@
#ifndef CLRSYNC_CORE_THEME_THEME_RENDERER_HPP #ifndef CLRSYNC_CORE_THEME_THEME_RENDERER_HPP
#define CLRSYNC_CORE_THEME_THEME_RENDERER_HPP #define CLRSYNC_CORE_THEME_THEME_RENDERER_HPP
#include <core/config/config.hpp> #include "core/common/error.hpp"
#include <core/palette/palette_manager.hpp> #include "core/config/config.hpp"
#include <core/theme/template_manager.hpp> #include "core/palette/palette_manager.hpp"
#include "core/theme/template_manager.hpp"
#include <iostream>
#include <string> #include <string>
namespace clrsync::core namespace clrsync::core
@@ -18,39 +20,54 @@ template <typename FileType> class theme_renderer
m_template_manager = template_manager<FileType>(); m_template_manager = template_manager<FileType>();
} }
void apply_theme(const std::string &theme_name) Result<void> apply_theme(const std::string &theme_name)
{ {
auto palette = m_pal_manager.get_palette(theme_name); auto palette = m_pal_manager.get_palette(theme_name);
if (!palette) if (!palette)
throw std::runtime_error("Palette not found: " + theme_name); return Err<void>(error_code::palette_not_found, "Palette not found", theme_name);
apply_palette_to_all_templates(*palette); return apply_palette_to_all_templates(*palette);
} }
void apply_theme_from_path(const std::string &path)
Result<void> apply_theme_from_path(const std::string &path)
{ {
auto palette = m_pal_manager.load_palette_from_file(path); auto palette = m_pal_manager.load_palette_from_file(path);
apply_palette_to_all_templates(palette); return apply_palette_to_all_templates(palette);
} }
private: private:
palette_manager<FileType> m_pal_manager; palette_manager<FileType> m_pal_manager;
template_manager<FileType> m_template_manager; template_manager<FileType> m_template_manager;
void apply_palette_to_all_templates(const palette &pal) Result<void> apply_palette_to_all_templates(const palette &pal)
{ {
for (auto &t_pair : m_template_manager.templates()) for (auto &t_pair : m_template_manager.templates())
{ {
auto &tmpl = t_pair.second; auto &tmpl = t_pair.second;
if (!tmpl.enabled()) if (!tmpl.enabled())
continue; continue;
tmpl.load_template();
auto load_result = tmpl.load_template();
if (!load_result)
return load_result;
tmpl.apply_palette(pal); tmpl.apply_palette(pal);
tmpl.save_output();
auto save_result = tmpl.save_output();
if (!save_result)
return save_result;
if (!tmpl.reload_command().empty()) if (!tmpl.reload_command().empty())
{ {
std::system(tmpl.reload_command().c_str()); int result = std::system(tmpl.reload_command().c_str());
if (result != 0)
{
std::cerr << "Warning: Command " << tmpl.reload_command()
<< " failed with code " << result << "\n";
} }
} }
} }
return Ok();
}
}; };
} // namespace clrsync::core } // namespace clrsync::core

View File

@@ -1,5 +1,5 @@
#include "theme_template.hpp" #include "theme_template.hpp"
#include "core/utils.hpp" #include "core/common/utils.hpp"
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
@@ -8,8 +8,8 @@ namespace clrsync::core
{ {
theme_template::theme_template(const std::string &name, const std::string &template_path, theme_template::theme_template(const std::string &name, const std::string &template_path,
const std::string &out_path) const std::string &out_path)
: m_name(name), m_template_path(expand_user(template_path)), : m_name(name), m_template_path(normalize_path(template_path).string()),
m_output_path(expand_user(out_path)) m_output_path(normalize_path(out_path).string())
{ {
} }
@@ -30,7 +30,7 @@ const std::string &theme_template::template_path() const
void theme_template::set_template_path(const std::string &path) void theme_template::set_template_path(const std::string &path)
{ {
m_template_path = expand_user(path); m_template_path = normalize_path(path).string();
} }
const std::string &theme_template::output_path() const const std::string &theme_template::output_path() const
@@ -40,21 +40,26 @@ const std::string &theme_template::output_path() const
void theme_template::set_output_path(const std::string &path) void theme_template::set_output_path(const std::string &path)
{ {
m_output_path = expand_user(path); m_output_path = normalize_path(path).string();
} }
void theme_template::load_template() Result<void> theme_template::load_template()
{ {
if (!std::filesystem::exists(m_template_path)) if (!std::filesystem::exists(m_template_path))
{ {
std::cerr << "Template file '" << m_template_path << "' is missing\n"; return Err<void>(error_code::template_not_found, "Template file is missing",
return; m_template_path);
} }
std::ifstream input(m_template_path, std::ios::binary); std::ifstream input(m_template_path, std::ios::binary);
if (!input) if (!input)
throw std::runtime_error("Failed to open template file: " + m_template_path); {
return Err<void>(error_code::template_load_failed, "Failed to open template file",
m_template_path);
}
m_template_data.assign(std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()); m_template_data.assign(std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>());
return Ok();
} }
void theme_template::apply_palette(const core::palette &palette) void theme_template::apply_palette(const core::palette &palette)
@@ -87,14 +92,32 @@ void theme_template::apply_palette(const core::palette &palette)
} }
} }
void theme_template::save_output() const Result<void> theme_template::save_output() const
{
try
{ {
std::filesystem::create_directories(std::filesystem::path(m_output_path).parent_path()); std::filesystem::create_directories(std::filesystem::path(m_output_path).parent_path());
}
catch (const std::exception &e)
{
return Err<void>(error_code::dir_create_failed, e.what(), m_output_path);
}
std::ofstream output(m_output_path, std::ios::binary); std::ofstream output(m_output_path, std::ios::binary);
if (!output) if (!output)
throw std::runtime_error("Failed to write output file: " + m_output_path); {
return Err<void>(error_code::file_write_failed, "Failed to open output file for writing",
m_output_path);
}
output << m_processed_data; output << m_processed_data;
if (!output)
{
return Err<void>(error_code::file_write_failed, "Failed to write to output file",
m_output_path);
}
return Ok();
} }
const std::string &theme_template::raw_template() const const std::string &theme_template::raw_template() const

View File

@@ -1,7 +1,8 @@
#ifndef clrsync_CORE_IO_THEME_TEMPLATE_HPP #ifndef clrsync_CORE_IO_THEME_TEMPLATE_HPP
#define clrsync_CORE_IO_THEME_TEMPLATE_HPP #define clrsync_CORE_IO_THEME_TEMPLATE_HPP
#include <core/palette/palette.hpp> #include "core/common/error.hpp"
#include "core/palette/palette.hpp"
#include <string> #include <string>
namespace clrsync::core namespace clrsync::core
@@ -26,11 +27,11 @@ class theme_template
void set_output_path(const std::string &path); void set_output_path(const std::string &path);
void load_template(); Result<void> load_template();
void apply_palette(const core::palette &palette); void apply_palette(const core::palette &palette);
void save_output() const; Result<void> save_output() const;
const std::string &raw_template() const; const std::string &raw_template() const;

View File

@@ -1,41 +0,0 @@
#include "utils.hpp"
#include <iostream>
namespace clrsync::core
{
void print_color_keys()
{
for (const auto &key : clrsync::core::COLOR_KEYS)
{
std::cout << key << std::endl;
}
}
std::string get_default_config_path()
{
auto home = expand_user("~");
#ifdef _WIN32
return home + "\\.config\\clrsync\\config.toml";
#else
return home + "/.config/clrsync/config.toml";
#endif
}
std::string expand_user(const std::string &path)
{
if (!path.empty() && path[0] == '~')
{
#ifdef _WIN32
const char *home = std::getenv("USERPROFILE");
#else
const char *home = std::getenv("HOME");
#endif
if (!home)
return path;
return std::string(home) + path.substr(1);
}
return path;
}
} // namespace clrsync::core

View File

@@ -1,10 +0,0 @@
#include "version.hpp"
namespace clrsync::core
{
const std::string version_string()
{
return "v" + std::to_string(VERSION_MAJOR) + "." + std::to_string(VERSION_MINOR) + "." +
std::to_string(VERSION_PATCH);
}
} // namespace clrsync::core

83
src/gui/CMakeLists.txt Normal file
View File

@@ -0,0 +1,83 @@
set(GUI_SOURCES
main.cpp
views/color_scheme_editor.cpp
views/color_table_renderer.cpp
views/preview_renderer.cpp
controllers/theme_applier.cpp
views/template_editor.cpp
controllers/palette_controller.cpp
controllers/template_controller.cpp
helpers/imgui_helpers.cpp
helpers/imgui_helpers.hpp
views/about_window.cpp
views/settings_window.cpp
platform/windows/font_loader_windows.cpp
platform/linux/font_loader_linux.cpp
platform/macos/font_loader_macos.cpp
platform/linux/file_browser_linux.cpp
platform/windows/file_browser_windows.cpp
${CMAKE_SOURCE_DIR}/lib/color_text_edit/TextEditor.cpp
platform/linux/font_loader_linux.cpp
platform/macos/font_loader_macos.cpp
platform/windows/font_loader_windows.cpp
backend/glfw_opengl.cpp
ui_manager.cpp
)
if(MACOS)
list(APPEND GUI_SOURCES
platform/macos/file_browser_macos.mm
)
endif()
if(WIN32)
add_executable(clrsync_gui WIN32 ${GUI_SOURCES})
else()
add_executable(clrsync_gui ${GUI_SOURCES})
endif()
target_include_directories(clrsync_gui PRIVATE
${CMAKE_SOURCE_DIR}/src
SYSTEM ${CMAKE_SOURCE_DIR}/lib
)
if(WIN32)
target_link_libraries(clrsync_gui PRIVATE
clrsync_core
glfw
imgui
OpenGL::GL
shell32
ole32
uuid
comdlg32
shlwapi
)
if (MSVC)
target_link_options(clrsync_gui PRIVATE /ENTRY:mainCRTStartup)
endif()
elseif(APPLE)
target_link_libraries(clrsync_gui PRIVATE
clrsync_core
glfw
imgui
OpenGL::GL
"-framework Cocoa"
)
else()
target_link_libraries(clrsync_gui PRIVATE
clrsync_core
imgui
${GLFW_LIBRARIES}
${WAYLAND_LIBS}
X11
Xrandr
Xi
Fontconfig::Fontconfig
OpenGL::GL
${GTK3_LIBRARIES}
)
target_include_directories(clrsync_gui PRIVATE ${GTK3_INCLUDE_DIRS})
target_compile_options(clrsync_gui PRIVATE ${GTK3_CFLAGS_OTHER})
endif()

View File

@@ -1,17 +0,0 @@
#ifndef CLRSYNC_GUI_ABOUT_WINDOW_HPP
#define CLRSYNC_GUI_ABOUT_WINDOW_HPP
class about_window
{
public:
about_window();
void render();
void show() { m_visible = true; }
void hide() { m_visible = false; }
bool is_visible() const { return m_visible; }
private:
bool m_visible{false};
};
#endif // CLRSYNC_GUI_ABOUT_WINDOW_HPP

View File

@@ -0,0 +1,38 @@
#ifndef CLRSYNC_BACKEND_HPP
#define CLRSYNC_BACKEND_HPP
#include <string>
namespace clrsync::gui::backend{
struct window_config{
std::string title = "clrsync";
int width = 1280;
int height = 720;
bool resizable = true;
bool decorated = true;
bool transparent_framebuffer = true;
float clear_color[4] = {0.1f, 0.1f, 0.1f, 0.0f};
};
class backend_interface{
public:
virtual ~backend_interface() = default;
virtual bool initialize(const window_config& config) = 0;
virtual void shutdown() = 0;
virtual bool should_close() const = 0;
virtual void begin_frame() = 0;
virtual void end_frame() = 0;
virtual void* get_native_window() const = 0;
virtual void* get_graphics_context() const = 0;
virtual bool init_imgui_backend() = 0;
virtual void shutdown_imgui_backend() = 0;
virtual void imgui_new_frame() = 0;
virtual void imgui_render_draw_data(void* draw_data) = 0;
};
}
#endif // CLRSYNC_BACKEND_HPP

View File

@@ -0,0 +1,141 @@
#include "gui/backend/glfw_opengl.hpp"
#include <iostream>
#include <GLFW/glfw3.h>
#include <imgui.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_opengl3.h>
namespace clrsync::gui::backend
{
glfw_opengl_backend::glfw_opengl_backend() = default;
glfw_opengl_backend::~glfw_opengl_backend()
{
glfw_opengl_backend::shutdown();
}
bool glfw_opengl_backend::initialize(const window_config &config)
{
glfwSetErrorCallback([](int error, const char* description) {
std::cerr << "GLFW Error " << error << ": " << description << std::endl;
});
if (!glfwInit())
{
std::cerr << "Failed to initialize GLFW" << std::endl;
return false;
}
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
#ifdef __APPLE__
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
#else
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
#endif
glfwWindowHint(GLFW_RESIZABLE, config.resizable ? GLFW_TRUE : GLFW_FALSE);
glfwWindowHint(GLFW_DECORATED, config.decorated ? GLFW_TRUE : GLFW_FALSE);
glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, config.transparent_framebuffer ? GLFW_TRUE : GLFW_FALSE);
m_window = glfwCreateWindow(config.width, config.height, config.title.c_str(), nullptr, nullptr);
if (!m_window)
{
std::cerr << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return false;
}
glfwMakeContextCurrent(m_window);
glfwSwapInterval(1);
return true;
}
void glfw_opengl_backend::shutdown()
{
if (m_window)
{
glfwDestroyWindow(m_window);
m_window = nullptr;
}
glfwTerminate();
}
bool glfw_opengl_backend::should_close() const
{
return m_window && glfwWindowShouldClose(m_window);
}
void glfw_opengl_backend::begin_frame()
{
glfwPollEvents();
if (m_window)
{
int display_w, display_h;
glfwGetFramebufferSize(m_window, &display_w, &display_h);
glViewport(0, 0, display_w, display_h);
glClearColor(0.1f, 0.1f, 0.1f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
}
}
void glfw_opengl_backend::end_frame()
{
if (m_window)
{
glfwSwapBuffers(m_window);
}
}
void *glfw_opengl_backend::get_native_window() const
{
return static_cast<void *>(m_window);
}
void *glfw_opengl_backend::get_graphics_context() const
{
return static_cast<void *>(m_window);
}
bool glfw_opengl_backend::init_imgui_backend()
{
if (!m_window)
return false;
if (!ImGui_ImplGlfw_InitForOpenGL(m_window, true))
return false;
#ifdef __APPLE__
const char* glsl_version = "#version 150";
#else
const char* glsl_version = "#version 120";
#endif
if (!ImGui_ImplOpenGL3_Init(glsl_version))
{
ImGui_ImplGlfw_Shutdown();
return false;
}
return true;
}
void glfw_opengl_backend::shutdown_imgui_backend()
{
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
}
void glfw_opengl_backend::imgui_new_frame()
{
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
}
void glfw_opengl_backend::imgui_render_draw_data(void* draw_data)
{
ImGui_ImplOpenGL3_RenderDrawData(static_cast<ImDrawData*>(draw_data));
}
} // namespace clrsync::gui::backend

View File

@@ -0,0 +1,36 @@
#ifndef CLRSYNC_GLFW_OPENGL_HPP
#define CLRSYNC_GLFW_OPENGL_HPP
#define GL_SILENCE_DEPRECATION
#include <GLFW/glfw3.h>
#include "gui/backend/backend.hpp"
namespace clrsync::gui::backend{
class glfw_opengl_backend : public backend_interface{
public:
glfw_opengl_backend();
~glfw_opengl_backend();
bool initialize(const window_config& config) override;
void shutdown() override;
bool should_close() const override;
void begin_frame() override;
void end_frame() override;
void* get_native_window() const override;
void* get_graphics_context() const override;
bool init_imgui_backend() override;
void shutdown_imgui_backend() override;
void imgui_new_frame() override;
void imgui_render_draw_data(void* draw_data) override;
private:
GLFWwindow* m_window = nullptr;
};
}
#endif // CLRSYNC_GLFW_OPENGL_HPP

View File

@@ -1,555 +0,0 @@
#include "color_scheme_editor.hpp"
#include "template_editor.hpp"
#include "color_text_edit/TextEditor.h"
#include "imgui.h"
#include <iostream>
#include <ranges>
color_scheme_editor::color_scheme_editor()
{
m_editor.SetLanguageDefinition(TextEditor::LanguageDefinition::CPlusPlus());
m_editor.SetText(R"(#include <iostream>
#include <string>
#include <vector>
#include <filesystem>
#include <cstdlib>
namespace fs = std::filesystem;
// Expands ~ to the user's home directory
std::string expand_user(const std::string &path)
{
if (path.empty()) return "";
std::string result;
if (path[0] == '~')
{
#ifdef _WIN32
const char* home = std::getenv("USERPROFILE");
#else
const char* home = std::getenv("HOME");
#endif
result = home ? std::string(home) : "~";
result += path.substr(1);
}
else
{
result = path;
}
return result;
}
// Lists all files in a directory
std::vector<std::string> list_files(const std::string &dir_path)
{
std::vector<std::string> files;
try
{
for (const auto &entry : fs::directory_iterator(dir_path))
{
if (entry.is_regular_file())
files.push_back(entry.path().string());
}
}
catch (const std::exception &e)
{
std::cerr << "Error: " << e.what() << std::endl;
}
return files;
}
int main()
{
std::string path = expand_user("~/Documents");
std::cout << "Listing files in: " << path << std::endl;
auto files = list_files(path);
for (const auto &f : files)
std::cout << " " << f << std::endl;
return 0;
})");
m_editor.SetShowWhitespaces(false);
const auto &palettes = m_controller.palettes();
const auto &current = m_controller.current_palette();
if (!current.colors().empty())
{
apply_palette_to_imgui();
apply_palette_to_editor();
}
else
{
std::cout << "WARNING: No palette loaded, skipping theme application\n";
}
}
void color_scheme_editor::notify_palette_changed()
{
if (m_template_editor)
{
m_template_editor->apply_current_palette(m_controller.current_palette());
}
}
void color_scheme_editor::render_controls_and_colors()
{
ImGui::Begin("Color Schemes");
render_controls();
ImGui::Separator();
ImGui::BeginChild("ColorTableContent", ImVec2(0, 0), false);
render_color_table();
ImGui::EndChild();
ImGui::End();
}
void color_scheme_editor::render_preview()
{
ImGui::Begin("Color Preview");
render_preview_content();
ImGui::End();
}
void color_scheme_editor::render_controls()
{
const auto &current = m_controller.current_palette();
const auto &palettes = m_controller.palettes();
const float avail_width = ImGui::GetContentRegionAvail().x;
ImGui::Text("Color Scheme:");
ImGui::SameLine();
ImGui::SetNextItemWidth(std::min(200.0f, avail_width * 0.3f));
if (ImGui::BeginCombo("##scheme", current.name().c_str()))
{
for (const auto &name : palettes | std::views::keys)
{
const bool selected = current.name() == name;
if (ImGui::Selectable(name.c_str(), selected))
{
m_controller.select_palette(name);
apply_palette_to_imgui();
apply_palette_to_editor();
notify_palette_changed();
}
if (selected)
ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
ImGui::SameLine();
static char new_palette_name_buf[128] = "";
if (ImGui::Button("New"))
{
new_palette_name_buf[0] = 0;
ImGui::OpenPopup("New Palette");
}
if (ImGui::BeginPopupModal("New Palette", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::Text("New palette name:");
ImGui::InputText("##new_palette_input", new_palette_name_buf,
IM_ARRAYSIZE(new_palette_name_buf));
ImGui::Separator();
if (ImGui::Button("Create", ImVec2(120, 0)))
{
if (strlen(new_palette_name_buf) > 0)
{
m_controller.create_palette(new_palette_name_buf);
apply_palette_to_imgui();
apply_palette_to_editor();
notify_palette_changed();
m_controller.select_palette(new_palette_name_buf);
new_palette_name_buf[0] = 0;
}
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(120, 0)))
{
new_palette_name_buf[0] = 0;
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
ImGui::SameLine();
if (ImGui::Button("Save"))
{
m_controller.save_current_palette();
}
ImGui::SameLine();
if (ImGui::Button("Delete"))
{
m_controller.delete_current_palette();
}
ImGui::SameLine();
if (ImGui::Button("Apply"))
{
m_controller.apply_current_theme();
}
}
void color_scheme_editor::render_color_table()
{
const auto &current = m_controller.current_palette();
if (current.colors().empty())
{
ImGui::Text("No palette loaded");
return;
}
ImGui::Text("Color Variables");
ImGui::Separator();
auto render_color_row = [&](const std::string &name) {
const auto &colors = current.colors();
auto it = colors.find(name);
if (it == colors.end())
return;
const clrsync::core::color &col = it->second;
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::TextUnformatted(name.c_str());
ImGui::TableSetColumnIndex(1);
{
std::string hex_str = col.to_hex_string();
char buf[9];
strncpy(buf, hex_str.c_str(), sizeof(buf));
buf[8] = 0;
ImGui::SetNextItemWidth(-FLT_MIN);
if (ImGui::InputText(("##hex_" + name).c_str(), buf, sizeof(buf),
ImGuiInputTextFlags_CharsUppercase))
{
try
{
clrsync::core::color new_color;
new_color.from_hex_string(buf);
m_controller.set_color(name, new_color);
apply_palette_to_imgui();
apply_palette_to_editor();
notify_palette_changed();
}
catch (...)
{
}
}
}
ImGui::TableSetColumnIndex(2);
ImGui::PushID(name.c_str());
float c[4] = {((col.hex() >> 24) & 0xFF) / 255.0f, ((col.hex() >> 16) & 0xFF) / 255.0f,
((col.hex() >> 8) & 0xFF) / 255.0f, (col.hex() & 0xFF) / 255.0f};
ImGui::SetNextItemWidth(-FLT_MIN);
if (ImGui::ColorEdit4(("##color_" + name).c_str(), c,
ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel |
ImGuiColorEditFlags_AlphaBar))
{
uint32_t r = (uint32_t)(c[0] * 255.0f);
uint32_t g = (uint32_t)(c[1] * 255.0f);
uint32_t b = (uint32_t)(c[2] * 255.0f);
uint32_t a = (uint32_t)(c[3] * 255.0f);
uint32_t hex = (r << 24) | (g << 16) | (b << 8) | a;
m_controller.set_color(name, clrsync::core::color(hex));
apply_palette_to_imgui();
apply_palette_to_editor();
notify_palette_changed();
}
ImGui::PopID();
};
auto draw_table = [&](const char *title, const std::vector<const char *> &keys) {
ImGui::TextUnformatted(title);
if (ImGui::BeginTable(title, 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg))
{
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 160.0f);
ImGui::TableSetupColumn("HEX", ImGuiTableColumnFlags_WidthFixed, 90.0f);
ImGui::TableSetupColumn("Preview", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableHeadersRow();
for (auto *k : keys)
render_color_row(k);
ImGui::EndTable();
}
ImGui::Spacing();
};
draw_table("General UI", {"background", "on_background", "surface", "on_surface",
"surface_variant", "on_surface_varuant", "foreground",
"cursor", "accent"});
draw_table("Borders", {"border_focused", "border"});
draw_table("Semantic Colors", {"success", "info", "warning", "error",
"on_success", "on_info", "on_warning", "on_error"});
draw_table("Editor", {"editor_background", "editor_command", "editor_comment",
"editor_disabled", "editor_emphasis", "editor_error",
"editor_inactive", "editor_line_number", "editor_link",
"editor_main", "editor_selected", "editor_selection_inactive",
"editor_string", "editor_success", "editor_warning"});
draw_table("Terminal (Base16)", {"base00", "base01", "base02", "base03",
"base04", "base05", "base06", "base07",
"base08", "base09", "base0A", "base0B",
"base0C", "base0D", "base0E", "base0F"});
}
void color_scheme_editor::render_preview_content()
{
const auto &current = m_controller.current_palette();
if (current.colors().empty())
{
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Current palette is empty");
return;
}
auto get_color = [&](const std::string &key) -> ImVec4 {
auto it = current.colors().find(key);
if (it != current.colors().end())
{
const auto &col = it->second;
const uint32_t hex = col.hex();
return {((hex >> 24) & 0xFF) / 255.0f, ((hex >> 16) & 0xFF) / 255.0f,
((hex >> 8) & 0xFF) / 255.0f, ((hex) & 0xFF) / 255.0f};
}
return {1, 1, 1, 1};
};
const ImVec4 editor_bg = get_color("editor_background");
const ImVec4 fg = get_color("foreground");
const ImVec4 accent = get_color("accent");
const ImVec4 border = get_color("border");
const ImVec4 error = get_color("error");
const ImVec4 warning = get_color("warning");
const ImVec4 success = get_color("success");
const ImVec4 info = get_color("info");
const float avail_height = ImGui::GetContentRegionAvail().y;
const float code_preview_height = std::max(250.0f, avail_height * 0.55f);
ImGui::Text("Code Editor:");
m_editor.Render("##CodeEditor", ImVec2(0, code_preview_height), true);
ImGui::Spacing();
ImGui::Text("Terminal Preview:");
ImGui::PushStyleColor(ImGuiCol_ChildBg, editor_bg);
ImGui::BeginChild("TerminalPreview", ImVec2(0, 0), true);
ImGui::PushStyleColor(ImGuiCol_Border, border);
struct term_line
{
const char *text{};
ImVec4 col;
};
term_line term_lines[] = {
{"$ ls -la", fg},
{"drwxr-xr-x 5 user group 4096 Dec 2 10:30 .", accent},
{"Build successful", success},
{"Error: file not found", error},
{"Warning: low disk space", warning},
{"Info: update available", info},
};
for (auto &[text, col] : term_lines)
{
ImGui::TextColored(col, "%s", text);
}
ImGui::PopStyleColor(2);
ImGui::EndChild();
}
void color_scheme_editor::apply_palette_to_editor()
{
const auto &current = m_controller.current_palette();
if (current.colors().empty())
return;
auto get_color_u32 = [&](const std::string &key, const std::string &fallback = "") -> uint32_t {
auto it = current.colors().find(key);
if (it == current.colors().end() && !fallback.empty())
{
it = current.colors().find(fallback);
}
if (it != current.colors().end())
{
const auto &col = it->second;
const uint32_t hex = col.hex();
// Convert from RRGGBBAA to AABBGGRR (ImGui format)
const uint32_t r = (hex >> 24) & 0xFF;
const uint32_t g = (hex >> 16) & 0xFF;
const uint32_t b = (hex >> 8) & 0xFF;
const uint32_t a = hex & 0xFF;
return (a << 24) | (b << 16) | (g << 8) | r;
}
return 0xFFFFFFFF;
};
auto palette = m_editor.GetPalette();
palette[int(TextEditor::PaletteIndex::Default)] = get_color_u32("editor_main");
palette[int(TextEditor::PaletteIndex::Keyword)] = get_color_u32("editor_command");
palette[int(TextEditor::PaletteIndex::Number)] = get_color_u32("editor_warning");
palette[int(TextEditor::PaletteIndex::String)] = get_color_u32("editor_string");
palette[int(TextEditor::PaletteIndex::CharLiteral)] = get_color_u32("editor_string");
palette[int(TextEditor::PaletteIndex::Punctuation)] = get_color_u32("editor_main");
palette[int(TextEditor::PaletteIndex::Preprocessor)] = get_color_u32("editor_emphasis");
palette[int(TextEditor::PaletteIndex::Identifier)] = get_color_u32("editor_main");
palette[int(TextEditor::PaletteIndex::KnownIdentifier)] = get_color_u32("editor_link");
palette[int(TextEditor::PaletteIndex::PreprocIdentifier)] = get_color_u32("editor_link");
palette[int(TextEditor::PaletteIndex::Comment)] = get_color_u32("editor_comment");
palette[int(TextEditor::PaletteIndex::MultiLineComment)] = get_color_u32("editor_comment");
palette[int(TextEditor::PaletteIndex::Background)] = get_color_u32("editor_background");
palette[int(TextEditor::PaletteIndex::Cursor)] = get_color_u32("cursor");
palette[int(TextEditor::PaletteIndex::Selection)] = get_color_u32("editor_selected");
palette[int(TextEditor::PaletteIndex::ErrorMarker)] = get_color_u32("editor_error");
palette[int(TextEditor::PaletteIndex::Breakpoint)] = get_color_u32("editor_error");
palette[int(TextEditor::PaletteIndex::LineNumber)] = get_color_u32("editor_line_number");
palette[int(TextEditor::PaletteIndex::CurrentLineFill)] = get_color_u32("surface_variant");
palette[int(TextEditor::PaletteIndex::CurrentLineFillInactive)] = get_color_u32("surface");
palette[int(TextEditor::PaletteIndex::CurrentLineEdge)] = get_color_u32("border_focused");
m_editor.SetPalette(palette);
}
void color_scheme_editor::apply_palette_to_imgui() const
{
const auto &current = m_controller.current_palette();
if (current.colors().empty())
return;
auto getColor = [&](const std::string &key, const std::string &fallback = "") -> ImVec4 {
auto it = current.colors().find(key);
if (it == current.colors().end() && !fallback.empty())
{
it = current.colors().find(fallback);
}
if (it != current.colors().end())
{
const uint32_t hex = it->second.hex();
return {((hex >> 24) & 0xFF) / 255.0f, ((hex >> 16) & 0xFF) / 255.0f,
((hex >> 8) & 0xFF) / 255.0f, ((hex) & 0xFF) / 255.0f};
}
std::cout << "WARNING: Color key '" << key << "' not found!\n";
return {1, 1, 1, 1};
};
ImGuiStyle &style = ImGui::GetStyle();
const ImVec4 bg = getColor("background");
const ImVec4 surface = getColor("surface");
const ImVec4 surfaceVariant = getColor("surface_variant");
const ImVec4 fg = getColor("foreground");
const ImVec4 fgInactive = getColor("editor_inactive");
const ImVec4 accent = getColor("accent");
const ImVec4 border = getColor("border");
style.Colors[ImGuiCol_WindowBg] = bg;
style.Colors[ImGuiCol_ChildBg] = surface;
style.Colors[ImGuiCol_PopupBg] = surface;
style.Colors[ImGuiCol_Border] = border;
style.Colors[ImGuiCol_BorderShadow] = ImVec4(0, 0, 0, 0);
style.Colors[ImGuiCol_Text] = fg;
style.Colors[ImGuiCol_TextDisabled] = fgInactive;
style.Colors[ImGuiCol_Header] = surfaceVariant;
style.Colors[ImGuiCol_HeaderHovered] = ImVec4(accent.x, accent.y, accent.z, 0.8f);
style.Colors[ImGuiCol_HeaderActive] = accent;
style.Colors[ImGuiCol_Button] = surfaceVariant;
style.Colors[ImGuiCol_ButtonHovered] = ImVec4(accent.x, accent.y, accent.z, 0.6f);
style.Colors[ImGuiCol_ButtonActive] = accent;
style.Colors[ImGuiCol_FrameBg] = surfaceVariant;
style.Colors[ImGuiCol_FrameBgHovered] =
ImVec4(surfaceVariant.x * 1.1f, surfaceVariant.y * 1.1f, surfaceVariant.z * 1.1f, 1.0f);
style.Colors[ImGuiCol_FrameBgActive] =
ImVec4(surfaceVariant.x * 1.2f, surfaceVariant.y * 1.2f, surfaceVariant.z * 1.2f, 1.0f);
style.Colors[ImGuiCol_TitleBg] = surface;
style.Colors[ImGuiCol_TitleBgActive] = surfaceVariant;
style.Colors[ImGuiCol_TitleBgCollapsed] = surface;
style.Colors[ImGuiCol_ScrollbarBg] = surface;
style.Colors[ImGuiCol_ScrollbarGrab] = surfaceVariant;
style.Colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(accent.x, accent.y, accent.z, 0.6f);
style.Colors[ImGuiCol_ScrollbarGrabActive] = accent;
style.Colors[ImGuiCol_SliderGrab] = accent;
style.Colors[ImGuiCol_SliderGrabActive] =
ImVec4(accent.x * 1.2f, accent.y * 1.2f, accent.z * 1.2f, 1.0f);
style.Colors[ImGuiCol_CheckMark] = accent;
style.Colors[ImGuiCol_ResizeGrip] = surfaceVariant;
style.Colors[ImGuiCol_ResizeGripHovered] = ImVec4(accent.x, accent.y, accent.z, 0.6f);
style.Colors[ImGuiCol_ResizeGripActive] = accent;
style.Colors[ImGuiCol_Tab] = surface;
style.Colors[ImGuiCol_TabHovered] = ImVec4(accent.x, accent.y, accent.z, 0.8f);
style.Colors[ImGuiCol_TabActive] = surfaceVariant;
style.Colors[ImGuiCol_TabUnfocused] = surface;
style.Colors[ImGuiCol_TabUnfocusedActive] = surfaceVariant;
style.Colors[ImGuiCol_TabSelectedOverline] = accent;
style.Colors[ImGuiCol_TableHeaderBg] = surfaceVariant;
style.Colors[ImGuiCol_TableBorderStrong] = border;
style.Colors[ImGuiCol_TableBorderLight] =
ImVec4(border.x * 0.7f, border.y * 0.7f, border.z * 0.7f, border.w);
style.Colors[ImGuiCol_TableRowBg] = ImVec4(0, 0, 0, 0);
style.Colors[ImGuiCol_TableRowBgAlt] = ImVec4(fg.x, fg.y, fg.z, 0.06f);
style.Colors[ImGuiCol_Separator] = border;
style.Colors[ImGuiCol_SeparatorHovered] = accent;
style.Colors[ImGuiCol_SeparatorActive] = accent;
style.Colors[ImGuiCol_MenuBarBg] = surface;
style.Colors[ImGuiCol_DockingPreview] = ImVec4(accent.x, accent.y, accent.z, 0.7f);
style.Colors[ImGuiCol_DockingEmptyBg] = bg;
}

View File

@@ -1,33 +0,0 @@
#ifndef CLRSYNC_GUI_COLOR_SCHEME_EDITOR_HPP
#define CLRSYNC_GUI_COLOR_SCHEME_EDITOR_HPP
#include "color_text_edit/TextEditor.h"
#include "palette_controller.hpp"
class template_editor;
class color_scheme_editor
{
public:
color_scheme_editor();
void render_controls_and_colors();
void render_preview();
void set_template_editor(template_editor* editor) { m_template_editor = editor; }
const palette_controller& controller() const { return m_controller; }
private:
void render_controls();
void render_color_table();
void render_preview_content();
void apply_palette_to_editor();
void apply_palette_to_imgui() const;
void notify_palette_changed();
palette_controller m_controller;
TextEditor m_editor;
template_editor* m_template_editor{nullptr};
};
#endif // CLRSYNC_GUI_COLOR_SCHEME_EDITOR_HPP

View File

@@ -1,4 +1,4 @@
#include "palette_controller.hpp" #include "gui/controllers/palette_controller.hpp"
#include "core/config/config.hpp" #include "core/config/config.hpp"
#include "core/theme/theme_renderer.hpp" #include "core/theme/theme_renderer.hpp"
@@ -11,9 +11,14 @@ palette_controller::palette_controller()
if (m_palettes.empty()) if (m_palettes.empty())
return; return;
try { auto default_theme = clrsync::core::config::instance().default_theme();
m_current_palette = m_palettes[clrsync::core::config::instance().default_theme()]; auto it = m_palettes.find(default_theme);
} catch (...) { if (it != m_palettes.end())
{
m_current_palette = it->second;
}
else
{
m_current_palette = m_palettes.begin()->second; m_current_palette = m_palettes.begin()->second;
} }
} }
@@ -21,15 +26,20 @@ palette_controller::palette_controller()
void palette_controller::select_palette(const std::string &name) void palette_controller::select_palette(const std::string &name)
{ {
auto it = m_palettes.find(name); auto it = m_palettes.find(name);
if (it != m_palettes.end()) { if (it != m_palettes.end())
{
m_current_palette = it->second; m_current_palette = it->second;
} }
} }
void palette_controller::create_palette(const std::string &name) void palette_controller::create_palette(const std::string &name)
{ {
clrsync::core::palette new_palette = m_palette_manager.load_palette_from_file(clrsync::core::config::get_data_dir().string() + "/palettes/cursed.toml"); clrsync::core::palette new_palette(name);
new_palette.set_name(name);
for (const auto &[key, hex_value] : clrsync::core::DEFAULT_COLORS)
{
new_palette.set_color(key, clrsync::core::color(hex_value));
}
auto dir = clrsync::core::config::instance().palettes_path(); auto dir = clrsync::core::config::instance().palettes_path();
m_palette_manager.save_palette_to_file(new_palette, dir); m_palette_manager.save_palette_to_file(new_palette, dir);
@@ -54,7 +64,7 @@ void palette_controller::delete_current_palette()
void palette_controller::apply_current_theme() const void palette_controller::apply_current_theme() const
{ {
clrsync::core::theme_renderer<clrsync::core::io::toml_file> theme_renderer; clrsync::core::theme_renderer<clrsync::core::io::toml_file> theme_renderer;
theme_renderer.apply_theme(m_current_palette.name()); (void)theme_renderer.apply_theme(m_current_palette.name());
} }
void palette_controller::set_color(const std::string &key, const clrsync::core::color &color) void palette_controller::set_color(const std::string &key, const clrsync::core::color &color)

View File

@@ -6,12 +6,19 @@
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
class palette_controller { class palette_controller
{
public: public:
palette_controller(); palette_controller();
const clrsync::core::palette& current_palette() const { return m_current_palette; } const clrsync::core::palette &current_palette() const
const std::unordered_map<std::string, clrsync::core::palette>& palettes() const { return m_palettes; } {
return m_current_palette;
}
const std::unordered_map<std::string, clrsync::core::palette> &palettes() const
{
return m_palettes;
}
void select_palette(const std::string &name); void select_palette(const std::string &name);
void create_palette(const std::string &name); void create_palette(const std::string &name);

View File

@@ -0,0 +1,64 @@
#include "gui/controllers/template_controller.hpp"
#include "core/config/config.hpp"
template_controller::template_controller()
{
m_templates = m_template_manager.templates();
}
void template_controller::set_template_enabled(const std::string &key, bool enabled)
{
auto it = m_templates.find(key);
if (it != m_templates.end())
{
it->second.set_enabled(enabled);
(void)clrsync::core::config::instance().update_template(key, it->second);
}
}
void template_controller::set_template_input_path(const std::string &key, const std::string &path)
{
auto it = m_templates.find(key);
if (it != m_templates.end())
{
it->second.set_template_path(path);
(void)clrsync::core::config::instance().update_template(key, it->second);
}
}
void template_controller::set_template_output_path(const std::string &key, const std::string &path)
{
auto it = m_templates.find(key);
if (it != m_templates.end())
{
it->second.set_output_path(path);
(void)clrsync::core::config::instance().update_template(key, it->second);
}
}
void template_controller::set_template_reload_command(const std::string &key,
const std::string &cmd)
{
auto it = m_templates.find(key);
if (it != m_templates.end())
{
it->second.set_reload_command(cmd);
(void)clrsync::core::config::instance().update_template(key, it->second);
}
}
bool template_controller::remove_template(const std::string &key)
{
auto result = clrsync::core::config::instance().remove_template(key);
if (result)
{
m_templates.erase(key);
return true;
}
return false;
}
void template_controller::refresh()
{
m_templates = m_template_manager.templates();
}

View File

@@ -1,19 +1,26 @@
#ifndef CLRSYNC_GUI_TEMPLATE_CONTROLLER_HPP #ifndef CLRSYNC_GUI_TEMPLATE_CONTROLLER_HPP
#define CLRSYNC_GUI_TEMPLATE_CONTROLLER_HPP #define CLRSYNC_GUI_TEMPLATE_CONTROLLER_HPP
#include "core/theme/theme_template.hpp"
#include "core/theme/template_manager.hpp"
#include "core/io/toml_file.hpp" #include "core/io/toml_file.hpp"
#include "core/theme/template_manager.hpp"
#include "core/theme/theme_template.hpp"
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
class template_controller { class template_controller
{
public: public:
template_controller(); template_controller();
[[nodiscard]] const std::unordered_map<std::string, clrsync::core::theme_template>& templates() const { return m_templates; } [[nodiscard]] const std::unordered_map<std::string, clrsync::core::theme_template> &templates()
const
{
return m_templates;
}
void set_template_enabled(const std::string &key, bool enabled); void set_template_enabled(const std::string &key, bool enabled);
void set_template_input_path(const std::string &key, const std::string &path);
void set_template_output_path(const std::string &key, const std::string &path); void set_template_output_path(const std::string &key, const std::string &path);
void set_template_reload_command(const std::string &key, const std::string &cmd); void set_template_reload_command(const std::string &key, const std::string &cmd);
bool remove_template(const std::string &key);
void refresh(); void refresh();
private: private:

View File

@@ -0,0 +1,162 @@
#include "gui/controllers/theme_applier.hpp"
#include "imgui.h"
namespace theme_applier
{
static uint32_t get_color_u32(const clrsync::core::palette &current, const std::string &key)
{
const auto &col = current.get_color(key);
const uint32_t hex = col.hex();
// Convert from RRGGBBAA to AABBGGRR (ImGui format)
const uint32_t r = (hex >> 24) & 0xFF;
const uint32_t g = (hex >> 16) & 0xFF;
const uint32_t b = (hex >> 8) & 0xFF;
const uint32_t a = hex & 0xFF;
return (a << 24) | (b << 16) | (g << 8) | r;
}
void apply_to_editor(TextEditor &editor, const clrsync::core::palette &current)
{
auto palette = editor.GetPalette();
palette[int(TextEditor::PaletteIndex::Default)] = get_color_u32(current, "editor_main");
palette[int(TextEditor::PaletteIndex::Keyword)] = get_color_u32(current, "editor_command");
palette[int(TextEditor::PaletteIndex::Number)] = get_color_u32(current, "editor_warning");
palette[int(TextEditor::PaletteIndex::String)] = get_color_u32(current, "editor_string");
palette[int(TextEditor::PaletteIndex::CharLiteral)] = get_color_u32(current, "editor_string");
palette[int(TextEditor::PaletteIndex::Punctuation)] = get_color_u32(current, "editor_main");
palette[int(TextEditor::PaletteIndex::Preprocessor)] =
get_color_u32(current, "editor_emphasis");
palette[int(TextEditor::PaletteIndex::Identifier)] = get_color_u32(current, "editor_main");
palette[int(TextEditor::PaletteIndex::KnownIdentifier)] = get_color_u32(current, "editor_link");
palette[int(TextEditor::PaletteIndex::PreprocIdentifier)] =
get_color_u32(current, "editor_link");
palette[int(TextEditor::PaletteIndex::Comment)] = get_color_u32(current, "editor_comment");
palette[int(TextEditor::PaletteIndex::MultiLineComment)] =
get_color_u32(current, "editor_comment");
palette[int(TextEditor::PaletteIndex::Background)] =
get_color_u32(current, "editor_background");
palette[int(TextEditor::PaletteIndex::Cursor)] = get_color_u32(current, "cursor");
palette[int(TextEditor::PaletteIndex::Selection)] = get_color_u32(current, "editor_selected");
palette[int(TextEditor::PaletteIndex::ErrorMarker)] = get_color_u32(current, "editor_error");
palette[int(TextEditor::PaletteIndex::Breakpoint)] = get_color_u32(current, "editor_error");
palette[int(TextEditor::PaletteIndex::LineNumber)] =
get_color_u32(current, "editor_line_number");
palette[int(TextEditor::PaletteIndex::CurrentLineFill)] =
get_color_u32(current, "surface_variant");
palette[int(TextEditor::PaletteIndex::CurrentLineFillInactive)] =
get_color_u32(current, "surface");
palette[int(TextEditor::PaletteIndex::CurrentLineEdge)] =
get_color_u32(current, "border_focused");
editor.SetPalette(palette);
}
void apply_to_imgui(const clrsync::core::palette &current)
{
auto getColor = [&](const std::string &key) -> ImVec4 {
const auto &col = current.get_color(key);
const uint32_t hex = col.hex();
return {((hex >> 24) & 0xFF) / 255.0f, ((hex >> 16) & 0xFF) / 255.0f,
((hex >> 8) & 0xFF) / 255.0f, ((hex) & 0xFF) / 255.0f};
};
ImGuiStyle &style = ImGui::GetStyle();
const ImVec4 bg = getColor("background");
const ImVec4 onBg = getColor("on_background");
const ImVec4 surface = getColor("surface");
const ImVec4 onSurface = getColor("on_surface");
const ImVec4 surfaceVariant = getColor("surface_variant");
const ImVec4 onSurfaceVariant = getColor("on_surface_variant");
const ImVec4 fg = getColor("foreground");
const ImVec4 fgInactive = getColor("editor_inactive");
const ImVec4 accent = getColor("accent");
const ImVec4 border = getColor("border");
const ImVec4 error = getColor("error");
const ImVec4 onError = getColor("on_error");
const ImVec4 success = getColor("success");
const ImVec4 onSuccess = getColor("on_success");
const ImVec4 warning = getColor("warning");
const ImVec4 onWarning = getColor("on_warning");
const ImVec4 info = getColor("info");
const ImVec4 onInfo = getColor("on_info");
style.Colors[ImGuiCol_WindowBg] = bg;
style.Colors[ImGuiCol_ChildBg] = surface;
style.Colors[ImGuiCol_PopupBg] = surface;
style.Colors[ImGuiCol_Border] = border;
style.Colors[ImGuiCol_BorderShadow] = ImVec4(0, 0, 0, 0);
style.Colors[ImGuiCol_Text] = onSurface;
style.Colors[ImGuiCol_TextDisabled] = onSurfaceVariant;
style.Colors[ImGuiCol_TextSelectedBg] = accent;
style.Colors[ImGuiCol_Header] = surfaceVariant;
style.Colors[ImGuiCol_HeaderHovered] = ImVec4(accent.x, accent.y, accent.z, 0.8f);
style.Colors[ImGuiCol_HeaderActive] = accent;
style.Colors[ImGuiCol_Button] = surfaceVariant;
style.Colors[ImGuiCol_ButtonHovered] = ImVec4(accent.x, accent.y, accent.z, 0.6f);
style.Colors[ImGuiCol_ButtonActive] = accent;
style.Colors[ImGuiCol_FrameBg] = surfaceVariant;
style.Colors[ImGuiCol_FrameBgHovered] =
ImVec4(surfaceVariant.x * 1.1f, surfaceVariant.y * 1.1f, surfaceVariant.z * 1.1f, 1.0f);
style.Colors[ImGuiCol_FrameBgActive] =
ImVec4(surfaceVariant.x * 1.2f, surfaceVariant.y * 1.2f, surfaceVariant.z * 1.2f, 1.0f);
style.Colors[ImGuiCol_TitleBg] = surface;
style.Colors[ImGuiCol_TitleBgActive] = surfaceVariant;
style.Colors[ImGuiCol_TitleBgCollapsed] = surface;
style.Colors[ImGuiCol_ScrollbarBg] = surface;
style.Colors[ImGuiCol_ScrollbarGrab] = surfaceVariant;
style.Colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(accent.x, accent.y, accent.z, 0.6f);
style.Colors[ImGuiCol_ScrollbarGrabActive] = accent;
style.Colors[ImGuiCol_SliderGrab] = accent;
style.Colors[ImGuiCol_SliderGrabActive] =
ImVec4(accent.x * 1.2f, accent.y * 1.2f, accent.z * 1.2f, 1.0f);
style.Colors[ImGuiCol_CheckMark] = accent;
style.Colors[ImGuiCol_ResizeGrip] = surfaceVariant;
style.Colors[ImGuiCol_ResizeGripHovered] = ImVec4(accent.x, accent.y, accent.z, 0.6f);
style.Colors[ImGuiCol_ResizeGripActive] = accent;
style.Colors[ImGuiCol_Tab] = surface;
style.Colors[ImGuiCol_TabHovered] = ImVec4(accent.x, accent.y, accent.z, 0.8f);
style.Colors[ImGuiCol_TabActive] = surfaceVariant;
style.Colors[ImGuiCol_TabUnfocused] = surface;
style.Colors[ImGuiCol_TabUnfocusedActive] = surfaceVariant;
style.Colors[ImGuiCol_TabSelectedOverline] = accent;
style.Colors[ImGuiCol_TableHeaderBg] = surfaceVariant;
style.Colors[ImGuiCol_TableBorderStrong] = border;
style.Colors[ImGuiCol_TableBorderLight] =
ImVec4(border.x * 0.7f, border.y * 0.7f, border.z * 0.7f, border.w);
style.Colors[ImGuiCol_TableRowBg] = ImVec4(0, 0, 0, 0);
style.Colors[ImGuiCol_TableRowBgAlt] =
ImVec4(onSurfaceVariant.x, onSurfaceVariant.y, onSurfaceVariant.z, 0.06f);
style.Colors[ImGuiCol_Separator] = border;
style.Colors[ImGuiCol_SeparatorHovered] = accent;
style.Colors[ImGuiCol_SeparatorActive] = accent;
style.Colors[ImGuiCol_MenuBarBg] = surface;
style.Colors[ImGuiCol_DockingPreview] = ImVec4(accent.x, accent.y, accent.z, 0.7f);
style.Colors[ImGuiCol_DockingEmptyBg] = bg;
}
} // namespace theme_applier

View File

@@ -0,0 +1,13 @@
#ifndef CLRSYNC_GUI_THEME_APPLIER_HPP
#define CLRSYNC_GUI_THEME_APPLIER_HPP
#include "color_text_edit/TextEditor.h"
#include "core/palette/palette.hpp"
namespace theme_applier
{
void apply_to_imgui(const clrsync::core::palette &pal);
void apply_to_editor(TextEditor &editor, const clrsync::core::palette &pal);
} // namespace theme_applier
#endif // CLRSYNC_GUI_THEME_APPLIER_HPP

View File

@@ -1,228 +0,0 @@
#include "font_loader.hpp"
#include "core/config/config.hpp"
#include "imgui_internal.h"
#include <imgui.h>
#include <algorithm>
#if defined(_WIN32)
#include <algorithm>
#include <windows.h>
#include <winreg.h>
static std::string search_registry_for_font(HKEY root_key, const char* subkey, const std::string& font_name_lower, const char* default_font_dir)
{
HKEY hKey;
LONG result = RegOpenKeyExA(root_key, subkey, 0, KEY_READ, &hKey);
if (result != ERROR_SUCCESS)
return {};
char value_name[512];
BYTE value_data[512];
DWORD value_name_size, value_data_size, type;
DWORD index = 0;
std::string found_path;
while (true)
{
value_name_size = sizeof(value_name);
value_data_size = sizeof(value_data);
result = RegEnumValueA(hKey, index++, value_name, &value_name_size, nullptr, &type, value_data, &value_data_size);
if (result != ERROR_SUCCESS)
break;
if (type != REG_SZ)
continue;
std::string reg_font_name = value_name;
std::transform(reg_font_name.begin(), reg_font_name.end(), reg_font_name.begin(), ::tolower);
if (reg_font_name.find(font_name_lower) != std::string::npos)
{
std::string font_file = reinterpret_cast<char*>(value_data);
// If path is not absolute, prepend default font directory
if (font_file.find(":\\") == std::string::npos)
{
found_path = std::string(default_font_dir) + "\\" + font_file;
}
else
{
found_path = font_file;
}
break;
}
}
RegCloseKey(hKey);
return found_path;
}
std::string font_loader::find_font_windows(const char* font_name)
{
std::string font_name_lower = font_name;
std::transform(font_name_lower.begin(), font_name_lower.end(), font_name_lower.begin(), ::tolower);
char windows_dir[MAX_PATH];
GetWindowsDirectoryA(windows_dir, MAX_PATH);
std::string system_fonts_dir = std::string(windows_dir) + "\\Fonts";
// First, try system-wide fonts (HKEY_LOCAL_MACHINE)
std::string path = search_registry_for_font(
HKEY_LOCAL_MACHINE,
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts",
font_name_lower,
system_fonts_dir.c_str()
);
if (!path.empty())
return path;
// If not found, try per-user fonts (HKEY_CURRENT_USER)
char local_appdata[MAX_PATH];
if (GetEnvironmentVariableA("LOCALAPPDATA", local_appdata, MAX_PATH) > 0)
{
std::string user_fonts_dir = std::string(local_appdata) + "\\Microsoft\\Windows\\Fonts";
path = search_registry_for_font(
HKEY_CURRENT_USER,
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts",
font_name_lower,
user_fonts_dir.c_str()
);
}
return path;
}
#endif
#if defined(__APPLE__)
#include <CoreText/CoreText.h>
std::vector<unsigned char> font_loader::load_font_macos(const char* font_name)
{
std::vector<unsigned char> out;
CFStringRef cf_name = CFStringCreateWithCString(nullptr, font_name, kCFStringEncodingUTF8);
if (!cf_name)
return out;
CTFontDescriptorRef desc = CTFontDescriptorCreateWithNameAndSize(cf_name, 12);
CFRelease(cf_name);
if (!desc)
return out;
CTFontRef font = CTFontCreateWithFontDescriptor(desc, 0, nullptr);
CFRelease(desc);
if (!font)
return out;
CFDataRef data = CTFontCopyTable(font, kCTFontTableCFF, 0);
if (!data)
data = CTFontCopyTable(font, kCTFontTableHead, 0);
if (data)
{
CFIndex size = CFDataGetLength(data);
out.resize(size);
CFDataGetBytes(data, CFRangeMake(0, size), out.data());
CFRelease(data);
}
CFRelease(font);
return out;
}
#endif
#if !defined(_WIN32) && !defined(__APPLE__)
#include <fontconfig/fontconfig.h>
std::string font_loader::find_font_linux(const char* font_name)
{
FcInit();
FcPattern* pattern = FcNameParse(reinterpret_cast<const FcChar8*>(font_name));
if (!pattern)
return {};
FcConfigSubstitute(nullptr, pattern, FcMatchPattern);
FcDefaultSubstitute(pattern);
FcResult result;
FcPattern* match = FcFontMatch(nullptr, pattern, &result);
std::string out;
if (match)
{
FcChar8* file = nullptr;
if (FcPatternGetString(match, FC_FILE, 0, &file) == FcResultMatch)
out = reinterpret_cast<const char*>(file);
FcPatternDestroy(match);
}
FcPatternDestroy(pattern);
return out;
}
#endif
std::string font_loader::find_font_path(const char* font_name)
{
#if defined(_WIN32)
return find_font_windows(font_name);
#elif defined(__APPLE__)
(void)font_name;
return {};
#else
return find_font_linux(font_name);
#endif
}
ImFont* font_loader::load_font(const char* font_name, float size_px)
{
#if defined(__APPLE__)
std::vector<unsigned char> buf = load_font_macos(font_name);
if (buf.empty())
return nullptr;
return ImGui::GetIO().Fonts->AddFontFromMemoryTTF(
buf.data(),
static_cast<int>(buf.size()),
size_px
);
#else
std::string path = find_font_path(font_name);
if (path.empty())
return nullptr;
float scale = ImGui::GetIO().DisplayFramebufferScale.y;
return ImGui::GetIO().Fonts->AddFontFromFileTTF(
path.c_str(),
size_px * scale
);
#endif
}
void font_loader::push_default_font()
{
ImGui::PushFont(ImGui::GetDefaultFont(), clrsync::core::config::instance().font_size());
}
void font_loader::pop_font()
{
ImGui::PopFont();
}

View File

@@ -1,30 +0,0 @@
#ifndef CLRSYNC_GUI_SYSTEM_FONT_LOADER_HPP
#define CLRSYNC_GUI_SYSTEM_FONT_LOADER_HPP
#include <string>
#include <vector>
#include <imgui.h>
class font_loader
{
public:
font_loader() = default;
// Loads system font by name and returns an ImFont* or nullptr.
ImFont* load_font(const char* font_name, float size_px);
void push_default_font();
void pop_font();
private:
std::string find_font_path(const char* font_name);
#if defined(_WIN32)
static std::string find_font_windows(const char* font_name);
#elif defined(__APPLE__)
std::vector<unsigned char> load_font_macos(const char* font_name);
#else
std::string find_font_linux(const char* font_name);
#endif
};
#endif // CLRSYNC_GUI_SYSTEM_FONT_LOADER_HPP

View File

@@ -0,0 +1,266 @@
#include <iostream>
#include <string>
#include "GLFW/glfw3.h"
#include "gui/views/settings_window.hpp"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include "imgui_helpers.hpp"
#include "imgui_internal.h"
GLFWwindow *init_glfw()
{
glfwSetErrorCallback([](int error, const char *description) {
std::cerr << "GLFW Error " << error << ": " << description << std::endl;
});
if (!glfwInit())
{
std::cerr << "Failed to initialize GLFW\n";
return nullptr;
}
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
#ifdef __APPLE__
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
#else
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
#endif
glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_TRUE);
GLFWwindow *w = glfwCreateWindow(1280, 720, "clrsync", nullptr, nullptr);
if (!w)
{
std::cerr << "Failed to create GLFW window\n";
return nullptr;
}
glfwMakeContextCurrent(w);
glfwSwapInterval(1);
return w;
}
void init_imgui(GLFWwindow *window, const std::string &ini_path)
{
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO &io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
io.IniFilename = ini_path.c_str();
ImGui::StyleColorsDark();
ImGui_ImplGlfw_InitForOpenGL(window, true);
#ifdef __APPLE__
ImGui_ImplOpenGL3_Init("#version 150");
#else
ImGui_ImplOpenGL3_Init("#version 120");
#endif
}
void begin_frame()
{
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
}
void render_menu_bar(about_window *about, settings_window *settings)
{
if (ImGui::BeginMainMenuBar())
{
if (ImGui::BeginMenu("File"))
{
if (ImGui::MenuItem("Settings"))
{
if (settings)
settings->show();
}
ImGui::Separator();
if (ImGui::MenuItem("Exit"))
{
glfwSetWindowShouldClose(glfwGetCurrentContext(), GLFW_TRUE);
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Help"))
{
if (ImGui::MenuItem("About"))
{
if (about)
about->show();
}
ImGui::EndMenu();
}
ImGui::EndMainMenuBar();
}
}
void setup_main_dockspace(bool &first_time)
{
const ImGuiViewport *viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->Pos);
ImGui::SetNextWindowSize(viewport->Size);
ImGui::SetNextWindowViewport(viewport->ID);
constexpr ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_MenuBar;
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::Begin("MainDockSpace", nullptr, flags);
ImGui::PopStyleVar(3);
ImGuiID dockspace_id = ImGui::GetID("MainDockSpace");
if (first_time)
{
first_time = false;
ImGui::DockBuilderRemoveNode(dockspace_id);
ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
ImGui::DockBuilderSetNodeSize(dockspace_id, viewport->Size);
ImGuiID center, right;
ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Right, 0.5f, &right, &center);
ImGuiDockNode *center_node = ImGui::DockBuilderGetNode(center);
if (center_node)
{
center_node->LocalFlags |= ImGuiDockNodeFlags_CentralNode;
}
ImGui::DockBuilderDockWindow("Color Schemes", right);
ImGui::DockBuilderDockWindow("Color Preview", center);
ImGui::DockBuilderDockWindow("Templates", center);
ImGui::DockBuilderFinish(dockspace_id);
}
ImGui::DockSpace(dockspace_id, ImVec2{0, 0}, ImGuiDockNodeFlags_None);
ImGui::End();
}
void end_frame(GLFWwindow *window)
{
ImGui::Render();
int w, h;
glfwGetFramebufferSize(window, &w, &h);
glViewport(0, 0, w, h);
glClearColor(0.1f, 0.1f, 0.1f, 0.f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
glfwSwapBuffers(window);
}
void shutdown(GLFWwindow *window)
{
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
glfwDestroyWindow(window);
glfwTerminate();
}
namespace palette_utils
{
ImVec4 get_color(const clrsync::core::palette &pal, const std::string &key,
const std::string &fallback)
{
auto colors = pal.colors();
if (colors.empty())
return ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
auto it = colors.find(key);
if (it == colors.end() && !fallback.empty())
{
it = colors.find(fallback);
}
if (it != colors.end())
{
const auto &col = it->second;
const uint32_t hex = col.hex();
const float r = ((hex >> 24) & 0xFF) / 255.0f;
const float g = ((hex >> 16) & 0xFF) / 255.0f;
const float b = ((hex >> 8) & 0xFF) / 255.0f;
const float a = (hex & 0xFF) / 255.0f;
return ImVec4(r, g, b, a);
}
return ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
}
uint32_t get_color_u32(const clrsync::core::palette &pal, const std::string &key,
const std::string &fallback)
{
auto colors = pal.colors();
if (colors.empty())
return 0xFFFFFFFF;
auto it = colors.find(key);
if (it == colors.end() && !fallback.empty())
{
it = colors.find(fallback);
}
if (it != colors.end())
{
const auto &col = it->second;
const uint32_t hex = col.hex();
const uint32_t r = (hex >> 24) & 0xFF;
const uint32_t g = (hex >> 16) & 0xFF;
const uint32_t b = (hex >> 8) & 0xFF;
const uint32_t a = hex & 0xFF;
return (a << 24) | (b << 16) | (g << 8) | r;
}
return 0xFFFFFFFF;
}
bool render_delete_confirmation_popup(const std::string &popup_title, const std::string &item_name,
const std::string &item_type,
const clrsync::core::palette &pal,
const std::function<void()> &on_delete)
{
bool result = false;
if (ImGui::BeginPopupModal(popup_title.c_str(), nullptr, ImGuiWindowFlags_AlwaysAutoResize))
{
ImVec4 warning_color = get_color(pal, "warning", "accent");
ImGui::TextColored(warning_color, "Are you sure you want to delete '%s'?",
item_name.c_str());
ImGui::Text("This action cannot be undone.");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
float button_width = 120.0f;
float total_width = 2.0f * button_width + ImGui::GetStyle().ItemSpacing.x;
float window_width = ImGui::GetContentRegionAvail().x;
ImGui::SetCursorPosX((window_width - total_width) * 0.5f);
if (ImGui::Button("Delete", ImVec2(button_width, 0)))
{
on_delete();
result = true;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(button_width, 0)))
{
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
return result;
}
} // namespace palette_utils

View File

@@ -0,0 +1,42 @@
#ifndef CLRSYNC_IMGUI_HELPERS_HPP
#define CLRSYNC_IMGUI_HELPERS_HPP
#include "core/palette/palette.hpp"
#include "gui/views/about_window.hpp"
#include "imgui.h"
#include <functional>
#include <string>
struct GLFWwindow;
class settings_window;
GLFWwindow *init_glfw();
void init_imgui(GLFWwindow *window, const std::string &ini_path);
void begin_frame();
void setup_main_dockspace(bool &first_time);
void end_frame(GLFWwindow *window);
void shutdown(GLFWwindow *window);
void render_menu_bar(about_window *about, settings_window *settings);
namespace palette_utils
{
ImVec4 get_color(const clrsync::core::palette &pal, const std::string &key,
const std::string &fallback = "");
uint32_t get_color_u32(const clrsync::core::palette &pal, const std::string &key,
const std::string &fallback = "");
bool render_delete_confirmation_popup(const std::string &popup_title, const std::string &item_name,
const std::string &item_type,
const clrsync::core::palette &pal,
const std::function<void()> &on_delete);
} // namespace palette_utils
namespace imgui_helpers
{
inline ImVec4 get_palette_color(const clrsync::core::palette &pal, const std::string &key,
const std::string &fallback = "")
{
return palette_utils::get_color(pal, key, fallback);
}
} // namespace imgui_helpers
#endif // CLRSYNC_IMGUI_HELPERS_HPP

View File

@@ -1,154 +0,0 @@
#include <string>
#include "GLFW/glfw3.h"
#include "gui/settings_window.hpp"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include "imgui_helpers.hpp"
#include "imgui_internal.h"
GLFWwindow * init_glfw()
{
if (!glfwInit()) return nullptr;
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_TRUE);
GLFWwindow* w = glfwCreateWindow(1280, 720, "clrsync", nullptr, nullptr);
if (!w) return nullptr;
glfwMakeContextCurrent(w);
glfwSwapInterval(1);
return w;
}
void init_imgui(GLFWwindow* window, const std::string& ini_path)
{
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
io.IniFilename = ini_path.c_str();
ImGui::StyleColorsDark();
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init("#version 130");
}
void begin_frame()
{
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
}
void render_menu_bar(about_window* about, settings_window* settings)
{
if (ImGui::BeginMainMenuBar())
{
if (ImGui::BeginMenu("File"))
{
if (ImGui::MenuItem("Settings"))
{
if (settings)
settings->show();
}
ImGui::Separator();
if (ImGui::MenuItem("Exit"))
{
// Will be handled by checking window should close
glfwSetWindowShouldClose(glfwGetCurrentContext(), GLFW_TRUE);
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Help"))
{
if (ImGui::MenuItem("About"))
{
if (about)
about->show();
}
ImGui::EndMenu();
}
ImGui::EndMainMenuBar();
}
}
void setup_main_dockspace(bool& first_time)
{
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->Pos);
ImGui::SetNextWindowSize(viewport->Size);
ImGui::SetNextWindowViewport(viewport->ID);
constexpr ImGuiWindowFlags flags =
ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoNavFocus |
ImGuiWindowFlags_MenuBar;
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0,0));
ImGui::Begin("MainDockSpace", nullptr, flags);
ImGui::PopStyleVar(3);
ImGuiID dockspace_id = ImGui::GetID("MainDockSpace");
if (first_time)
{
first_time = false;
ImGui::DockBuilderRemoveNode(dockspace_id);
ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
ImGui::DockBuilderSetNodeSize(dockspace_id, viewport->Size);
ImGuiID center, right;
ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Right, 0.5f, &right, &center);
ImGuiDockNode* center_node = ImGui::DockBuilderGetNode(center);
if (center_node)
{
center_node->LocalFlags |= ImGuiDockNodeFlags_CentralNode;
}
ImGui::DockBuilderDockWindow("Color Schemes", right);
ImGui::DockBuilderDockWindow("Templates", center);
ImGui::DockBuilderDockWindow("Color Preview", center);
ImGui::DockBuilderFinish(dockspace_id);
}
ImGui::DockSpace(dockspace_id, ImVec2{0,0}, ImGuiDockNodeFlags_None);
ImGui::End();
}
void end_frame(GLFWwindow* window)
{
ImGui::Render();
int w, h;
glfwGetFramebufferSize(window, &w, &h);
glViewport(0, 0, w, h);
glClearColor(0.1f, 0.1f, 0.1f, 0.f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
glfwSwapBuffers(window);
}
void shutdown(GLFWwindow* window)
{
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
glfwDestroyWindow(window);
glfwTerminate();
}

View File

@@ -1,18 +0,0 @@
#ifndef CLRSYNC_IMGUI_HELPERS_HPP
#define CLRSYNC_IMGUI_HELPERS_HPP
#include "gui/about_window.hpp"
#include <string>
struct GLFWwindow;
class settings_window;
GLFWwindow * init_glfw();
void init_imgui(GLFWwindow* window, const std::string& ini_path);
void begin_frame();
void setup_main_dockspace(bool& first_time);
void end_frame(GLFWwindow* window);
void shutdown(GLFWwindow* window);
void render_menu_bar(about_window* about, settings_window* settings);
#endif // CLRSYNC_IMGUI_HELPERS_HPP

View File

@@ -1,38 +1,83 @@
#include <iostream>
#include <memory> #include <memory>
#include <GLFW/glfw3.h> #include <GLFW/glfw3.h>
#include <GLFW/glfw3native.h>
#include "core/common/error.hpp"
#include "core/common/utils.hpp"
#include "core/config/config.hpp" #include "core/config/config.hpp"
#include "core/io/toml_file.hpp" #include "core/io/toml_file.hpp"
#include "core/utils.hpp"
#include "color_scheme_editor.hpp"
#include "gui/font_loader.hpp"
#include "gui/settings_window.hpp"
#include "imgui_helpers.hpp"
#include "template_editor.hpp"
#include "about_window.hpp"
#include "gui/backend/glfw_opengl.hpp"
#include "gui/helpers/imgui_helpers.hpp"
#include "gui/platform/font_loader.hpp"
#include "gui/ui_manager.hpp"
#include "gui/views/about_window.hpp"
#include "gui/views/color_scheme_editor.hpp"
#include "gui/views/settings_window.hpp"
#include "gui/views/template_editor.hpp"
int main(int, char **) int main(int, char **)
{ {
auto config_path = clrsync::core::get_default_config_path(); auto config_path = clrsync::core::get_default_config_path();
auto conf = std::make_unique<clrsync::core::io::toml_file>(config_path); auto conf = std::make_unique<clrsync::core::io::toml_file>(config_path);
clrsync::core::config::instance().initialize(std::move(conf));
auto init_result = clrsync::core::config::instance().initialize(std::move(conf));
if (!init_result)
{
std::cerr << "Fatal error: " << init_result.error().description() << std::endl;
std::cerr
<< "Hint: Set CLRSYNC_CONFIG_PATH environment variable or ensure config exists at: "
<< config_path << std::endl;
return 1;
}
std::filesystem::path base = config_path; std::filesystem::path base = config_path;
static std::string ini_path = (base.parent_path() / "layout.ini").string(); static std::string ini_path = (base.parent_path() / "layout.ini").string();
bool first_time = !std::filesystem::exists(ini_path); bool first_time = !std::filesystem::exists(ini_path);
GLFWwindow* window = init_glfw(); printf("GLFW Version: %s\n", glfwGetVersionString());
if (!window) return 1;
init_imgui(window, ini_path); auto backend = clrsync::gui::backend::glfw_opengl_backend();
auto window_config = clrsync::gui::backend::window_config();
window_config.title = "clrsync";
window_config.width = 1280;
window_config.height = 720;
window_config.transparent_framebuffer = true;
if (!backend.initialize(window_config))
{
std::cerr << "Failed to initialize backend." << std::endl;
return 1;
}
std::cout << "GLFW runtime platform: ";
switch (glfwGetPlatform()) {
case GLFW_PLATFORM_WAYLAND: std::cout << "Wayland\n"; break;
case GLFW_PLATFORM_X11: std::cout << "X11\n"; break;
case GLFW_PLATFORM_COCOA: std::cout << "Cocoa\n"; break;
case GLFW_PLATFORM_WIN32: std::cout << "Win32\n"; break;
default: std::cout << "Unknown\n";
}
clrsync::gui::ui_manager ui_manager(&backend);
clrsync::gui::ui_config ui_cfg;
ui_cfg.ini_path = ini_path;
ui_cfg.enable_docking = true;
ui_cfg.enable_keyboard_nav = true;
if (!ui_manager.initialize(ui_cfg))
{
std::cerr << "Failed to initialize UI manager." << std::endl;
return 1;
}
font_loader loader; font_loader loader;
ImFont* font = ImFont *font = loader.load_font(clrsync::core::config::instance().font().c_str(),
loader.load_font(clrsync::core::config::instance().font().c_str(), clrsync::core::config::instance().font_size()); clrsync::core::config::instance().font_size());
if (font) if (font)
ImGui::GetIO().FontDefault = font; ImGui::GetIO().FontDefault = font;
@@ -43,25 +88,32 @@ int main(int, char**)
settings_window settingsWindow; settings_window settingsWindow;
colorEditor.set_template_editor(&templateEditor); colorEditor.set_template_editor(&templateEditor);
colorEditor.set_settings_window(&settingsWindow);
templateEditor.apply_current_palette(colorEditor.controller().current_palette()); templateEditor.apply_current_palette(colorEditor.controller().current_palette());
settingsWindow.set_palette(colorEditor.controller().current_palette());
while (!glfwWindowShouldClose(window)) while (!backend.should_close())
{ {
glfwPollEvents(); backend.begin_frame();
loader.push_default_font(); loader.push_default_font();
begin_frame(); ui_manager.begin_frame();
render_menu_bar(&aboutWindow, &settingsWindow); render_menu_bar(&aboutWindow, &settingsWindow);
setup_main_dockspace(first_time); setup_main_dockspace(first_time);
templateEditor.render();
colorEditor.render_controls_and_colors(); colorEditor.render_controls_and_colors();
colorEditor.render_preview(); colorEditor.render_preview();
templateEditor.render(); aboutWindow.render(colorEditor.controller().current_palette());
aboutWindow.render();
settingsWindow.render(); settingsWindow.render();
loader.pop_font(); loader.pop_font();
end_frame(window);
ui_manager.end_frame();
backend.end_frame();
} }
shutdown(window);
ui_manager.shutdown();
backend.shutdown();
return 0; return 0;
} }

View File

@@ -0,0 +1,21 @@
#ifndef CLRSYNC_GUI_FILE_BROWSER_HPP
#define CLRSYNC_GUI_FILE_BROWSER_HPP
#include <string>
#include <vector>
namespace file_dialogs
{
std::string open_file_dialog(const std::string &title = "Open File",
const std::string &initial_path = "",
const std::vector<std::string> &filters = {});
std::string save_file_dialog(const std::string &title = "Save File",
const std::string &initial_path = "",
const std::vector<std::string> &filters = {});
std::string select_folder_dialog(const std::string &title = "Select Folder",
const std::string &initial_path = "");
} // namespace file_dialogs
#endif // CLRSYNC_GUI_FILE_BROWSER_HPP

View File

@@ -0,0 +1,31 @@
#ifndef CLRSYNC_GUI_SYSTEM_FONT_LOADER_HPP
#define CLRSYNC_GUI_SYSTEM_FONT_LOADER_HPP
#include <imgui.h>
#include <string>
#include <vector>
class font_loader
{
public:
font_loader() = default;
ImFont *load_font(const char *font_name, float size_px);
void push_default_font();
void pop_font();
std::vector<std::string> get_system_fonts();
private:
std::string find_font_path(const char *font_name);
#if defined(_WIN32)
static std::string find_font_windows(const char *font_name);
#elif defined(__APPLE__)
std::vector<unsigned char> load_font_macos(const char *font_name);
#else
std::string find_font_linux(const char *font_name);
#endif
};
#endif // CLRSYNC_GUI_SYSTEM_FONT_LOADER_HPP

View File

@@ -0,0 +1,133 @@
#ifdef __linux__
#include "gui/platform/file_browser.hpp"
#include <gtk/gtk.h>
#include <filesystem>
namespace file_dialogs
{
std::string open_file_dialog(const std::string &title, const std::string &initial_path,
const std::vector<std::string> &filters)
{
if (!gtk_init_check(nullptr, nullptr))
{
return "";
}
GtkFileChooserNative *native = gtk_file_chooser_native_new(
title.c_str(), nullptr, GTK_FILE_CHOOSER_ACTION_OPEN, "_Open", "_Cancel");
GtkFileChooser *chooser = GTK_FILE_CHOOSER(native);
if (!initial_path.empty())
{
std::filesystem::path p(initial_path);
if (std::filesystem::exists(p))
{
if (std::filesystem::is_directory(p))
{
gtk_file_chooser_set_current_folder(chooser, initial_path.c_str());
}
else
{
gtk_file_chooser_set_current_folder(chooser, p.parent_path().c_str());
gtk_file_chooser_set_current_name(chooser, p.filename().c_str());
}
}
}
std::string result;
if (gtk_native_dialog_run(GTK_NATIVE_DIALOG(native)) == GTK_RESPONSE_ACCEPT)
{
char *filename = gtk_file_chooser_get_filename(chooser);
if (filename)
{
result = filename;
g_free(filename);
}
}
g_object_unref(native);
while (gtk_events_pending())
gtk_main_iteration();
return result;
}
std::string save_file_dialog(const std::string &title, const std::string &initial_path,
const std::vector<std::string> &filters)
{
if (!gtk_init_check(nullptr, nullptr))
{
return "";
}
GtkFileChooserNative *native = gtk_file_chooser_native_new(
title.c_str(), nullptr, GTK_FILE_CHOOSER_ACTION_SAVE, "_Save", "_Cancel");
GtkFileChooser *chooser = GTK_FILE_CHOOSER(native);
gtk_file_chooser_set_do_overwrite_confirmation(chooser, TRUE);
if (!initial_path.empty())
{
std::filesystem::path p(initial_path);
if (std::filesystem::exists(p.parent_path()))
{
gtk_file_chooser_set_current_folder(chooser, p.parent_path().c_str());
gtk_file_chooser_set_current_name(chooser, p.filename().c_str());
}
}
std::string result;
if (gtk_native_dialog_run(GTK_NATIVE_DIALOG(native)) == GTK_RESPONSE_ACCEPT)
{
char *filename = gtk_file_chooser_get_filename(chooser);
if (filename)
{
result = filename;
g_free(filename);
}
}
g_object_unref(native);
while (gtk_events_pending())
gtk_main_iteration();
return result;
}
std::string select_folder_dialog(const std::string &title, const std::string &initial_path)
{
if (!gtk_init_check(nullptr, nullptr))
{
return "";
}
GtkFileChooserNative *native = gtk_file_chooser_native_new(
title.c_str(), nullptr, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, "_Select", "_Cancel");
GtkFileChooser *chooser = GTK_FILE_CHOOSER(native);
if (!initial_path.empty() && std::filesystem::exists(initial_path))
{
gtk_file_chooser_set_current_folder(chooser, initial_path.c_str());
}
std::string result;
if (gtk_native_dialog_run(GTK_NATIVE_DIALOG(native)) == GTK_RESPONSE_ACCEPT)
{
char *filename = gtk_file_chooser_get_filename(chooser);
if (filename)
{
result = filename;
g_free(filename);
}
}
g_object_unref(native);
while (gtk_events_pending())
gtk_main_iteration();
return result;
}
} // namespace file_dialogs
#endif

View File

@@ -0,0 +1,95 @@
#ifdef __linux__
#include "core/config/config.hpp"
#include "gui/platform/font_loader.hpp"
#include "imgui_internal.h"
#include <algorithm>
#include <fontconfig/fontconfig.h>
#include <imgui.h>
std::string font_loader::find_font_linux(const char *font_name)
{
FcInit();
FcPattern *pattern = FcNameParse(reinterpret_cast<const FcChar8 *>(font_name));
if (!pattern)
return {};
FcConfigSubstitute(nullptr, pattern, FcMatchPattern);
FcDefaultSubstitute(pattern);
FcResult result;
FcPattern *match = FcFontMatch(nullptr, pattern, &result);
std::string out;
if (match)
{
FcChar8 *file = nullptr;
if (FcPatternGetString(match, FC_FILE, 0, &file) == FcResultMatch)
out = reinterpret_cast<const char *>(file);
FcPatternDestroy(match);
}
FcPatternDestroy(pattern);
return out;
}
std::string font_loader::find_font_path(const char *font_name)
{
return find_font_linux(font_name);
}
ImFont *font_loader::load_font(const char *font_name, float size_px)
{
std::string path = find_font_path(font_name);
if (path.empty())
return nullptr;
float scale = ImGui::GetIO().DisplayFramebufferScale.y;
return ImGui::GetIO().Fonts->AddFontFromFileTTF(path.c_str(), size_px * scale);
}
void font_loader::push_default_font()
{
ImGui::PushFont(ImGui::GetDefaultFont(), clrsync::core::config::instance().font_size());
}
void font_loader::pop_font()
{
ImGui::PopFont();
}
std::vector<std::string> font_loader::get_system_fonts()
{
std::vector<std::string> fonts;
FcInit();
FcPattern *pattern = FcPatternCreate();
FcObjectSet *os = FcObjectSetBuild(FC_FAMILY, nullptr);
FcFontSet *fs = FcFontList(nullptr, pattern, os);
if (fs)
{
for (int i = 0; i < fs->nfont; i++)
{
FcChar8 *family = nullptr;
if (FcPatternGetString(fs->fonts[i], FC_FAMILY, 0, &family) == FcResultMatch)
{
std::string font_name = reinterpret_cast<const char *>(family);
if (std::find(fonts.begin(), fonts.end(), font_name) == fonts.end())
fonts.push_back(font_name);
}
}
FcFontSetDestroy(fs);
}
FcObjectSetDestroy(os);
FcPatternDestroy(pattern);
std::sort(fonts.begin(), fonts.end());
return fonts;
}
#endif

View File

@@ -0,0 +1,78 @@
#ifdef __APPLE__
#include "gui/platform/file_browser.hpp"
#include <filesystem>
#include <Cocoa/Cocoa.h>
namespace file_dialogs {
std::string open_file_dialog(const std::string& title,
const std::string& initial_path,
const std::vector<std::string>& filters) {
@autoreleasepool {
NSOpenPanel* panel = [NSOpenPanel openPanel];
[panel setTitle:[NSString stringWithUTF8String:title.c_str()]];
[panel setCanChooseFiles:YES];
[panel setCanChooseDirectories:NO];
[panel setAllowsMultipleSelection:NO];
if (!initial_path.empty()) {
NSURL* url = [NSURL fileURLWithPath:[NSString stringWithUTF8String:initial_path.c_str()]];
[panel setDirectoryURL:url];
}
if ([panel runModal] == NSModalResponseOK) {
NSURL* url = [[panel URLs] objectAtIndex:0];
return std::string([[url path] UTF8String]);
}
}
return "";
}
std::string save_file_dialog(const std::string& title,
const std::string& initial_path,
const std::vector<std::string>& filters) {
@autoreleasepool {
NSSavePanel* panel = [NSSavePanel savePanel];
[panel setTitle:[NSString stringWithUTF8String:title.c_str()]];
if (!initial_path.empty()) {
std::filesystem::path p(initial_path);
if (std::filesystem::exists(p.parent_path())) {
NSURL* url = [NSURL fileURLWithPath:[NSString stringWithUTF8String:p.parent_path().c_str()]];
[panel setDirectoryURL:url];
[panel setNameFieldStringValue:[NSString stringWithUTF8String:p.filename().c_str()]];
}
}
if ([panel runModal] == NSModalResponseOK) {
NSURL* url = [panel URL];
return std::string([[url path] UTF8String]);
}
}
return "";
}
std::string select_folder_dialog(const std::string& title,
const std::string& initial_path) {
@autoreleasepool {
NSOpenPanel* panel = [NSOpenPanel openPanel];
[panel setTitle:[NSString stringWithUTF8String:title.c_str()]];
[panel setCanChooseFiles:NO];
[panel setCanChooseDirectories:YES];
[panel setAllowsMultipleSelection:NO];
if (!initial_path.empty()) {
NSURL* url = [NSURL fileURLWithPath:[NSString stringWithUTF8String:initial_path.c_str()]];
[panel setDirectoryURL:url];
}
if ([panel runModal] == NSModalResponseOK) {
NSURL* url = [[panel URLs] objectAtIndex:0];
return std::string([[url path] UTF8String]);
}
}
return "";
}
}
#endif

View File

@@ -0,0 +1,115 @@
#ifdef __APPLE__
#include "core/config/config.hpp"
#include "gui/platform/font_loader.hpp"
#include "imgui_internal.h"
#include <CoreText/CoreText.h>
#include <algorithm>
#include <imgui.h>
std::vector<unsigned char> font_loader::load_font_macos(const char *font_name)
{
std::vector<unsigned char> out;
CFStringRef cf_name = CFStringCreateWithCString(nullptr, font_name, kCFStringEncodingUTF8);
if (!cf_name)
return out;
CTFontDescriptorRef desc = CTFontDescriptorCreateWithNameAndSize(cf_name, 12);
CFRelease(cf_name);
if (!desc)
return out;
CFURLRef url = (CFURLRef)CTFontDescriptorCopyAttribute(desc, kCTFontURLAttribute);
CFRelease(desc);
if (!url)
return out;
CFDataRef data = nullptr;
Boolean success = CFURLCreateDataAndPropertiesFromResource(kCFAllocatorDefault, url, &data,
nullptr, nullptr, nullptr);
CFRelease(url);
if (success && data)
{
CFIndex size = CFDataGetLength(data);
if (size > 100)
{
out.resize(size);
CFDataGetBytes(data, CFRangeMake(0, size), out.data());
}
CFRelease(data);
}
return out;
}
std::string font_loader::find_font_path(const char *font_name)
{
(void)font_name;
return {};
}
ImFont *font_loader::load_font(const char *font_name, float size_px)
{
std::vector<unsigned char> buf = load_font_macos(font_name);
if (buf.empty())
return nullptr;
return ImGui::GetIO().Fonts->AddFontFromMemoryTTF(buf.data(), static_cast<int>(buf.size()),
size_px);
}
void font_loader::push_default_font()
{
ImGui::PushFont(ImGui::GetDefaultFont(), clrsync::core::config::instance().font_size());
}
void font_loader::pop_font()
{
ImGui::PopFont();
}
std::vector<std::string> font_loader::get_system_fonts()
{
std::vector<std::string> fonts;
CTFontCollectionRef collection = CTFontCollectionCreateFromAvailableFonts(nullptr);
if (collection)
{
CFArrayRef fontDescriptors = CTFontCollectionCreateMatchingFontDescriptors(collection);
CFRelease(collection);
if (fontDescriptors)
{
CFIndex count = CFArrayGetCount(fontDescriptors);
for (CFIndex i = 0; i < count; i++)
{
CTFontDescriptorRef descriptor =
(CTFontDescriptorRef)CFArrayGetValueAtIndex(fontDescriptors, i);
CFStringRef fontName = (CFStringRef)CTFontDescriptorCopyAttribute(
descriptor, kCTFontDisplayNameAttribute);
if (fontName)
{
char buffer[256];
if (CFStringGetCString(fontName, buffer, sizeof(buffer), kCFStringEncodingUTF8))
{
std::string font_name = buffer;
if (std::find(fonts.begin(), fonts.end(), font_name) == fonts.end())
fonts.push_back(font_name);
}
CFRelease(fontName);
}
}
CFRelease(fontDescriptors);
}
}
std::sort(fonts.begin(), fonts.end());
return fonts;
}
#endif

View File

@@ -0,0 +1,167 @@
#ifdef _WIN32
// clang-format off
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <commdlg.h>
#include <shlobj.h>
#include <shlwapi.h>
// clang-format on
#include "gui/platform/file_browser.hpp"
#include <filesystem>
namespace file_dialogs
{
std::string open_file_dialog(const std::string &title, const std::string &initial_path,
const std::vector<std::string> &filters)
{
OPENFILENAMEA ofn;
char file[MAX_PATH] = "";
std::string filter_str = "All Files (*.*)\0*.*\0";
ZeroMemory(&ofn, sizeof(ofn));
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = GetActiveWindow();
ofn.lpstrFile = file;
ofn.nMaxFile = sizeof(file);
ofn.lpstrFilter = filter_str.c_str();
ofn.nFilterIndex = 1;
ofn.lpstrTitle = title.c_str();
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR;
if (!initial_path.empty() && std::filesystem::exists(initial_path))
{
std::filesystem::path p(initial_path);
if (std::filesystem::is_directory(p))
{
ofn.lpstrInitialDir = initial_path.c_str();
}
else
{
std::string dir = p.parent_path().string();
std::string name = p.filename().string();
ofn.lpstrInitialDir = dir.c_str();
strncpy(file, name.c_str(), sizeof(file) - 1);
}
}
if (GetOpenFileNameA(&ofn))
{
return std::string(file);
}
return "";
}
std::string save_file_dialog(const std::string &title, const std::string &initial_path,
const std::vector<std::string> &filters)
{
OPENFILENAMEA ofn;
char file[MAX_PATH] = "";
std::string filter_str = "All Files\0*.*\0\0";
ZeroMemory(&ofn, sizeof(ofn));
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = GetActiveWindow();
ofn.lpstrFile = file;
ofn.nMaxFile = sizeof(file);
ofn.lpstrFilter = filter_str.c_str();
ofn.nFilterIndex = 1;
ofn.lpstrTitle = title.c_str();
ofn.Flags = OFN_PATHMUSTEXIST | OFN_OVERWRITEPROMPT | OFN_NOCHANGEDIR;
if (!initial_path.empty())
{
std::filesystem::path p(initial_path);
if (std::filesystem::exists(p) && std::filesystem::is_directory(p))
{
ofn.lpstrInitialDir = initial_path.c_str();
}
else
{
std::string dir = p.parent_path().string();
std::string name = p.filename().string();
if (std::filesystem::exists(dir))
{
ofn.lpstrInitialDir = dir.c_str();
strncpy(file, name.c_str(), sizeof(file) - 1);
}
}
}
if (GetSaveFileNameA(&ofn))
{
return std::string(file);
}
return "";
}
std::string select_folder_dialog(const std::string &title, const std::string &initial_path)
{
IFileOpenDialog *pFileOpen;
HRESULT hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_ALL, IID_IFileOpenDialog,
reinterpret_cast<void **>(&pFileOpen));
if (SUCCEEDED(hr))
{
DWORD dwFlags;
if (SUCCEEDED(pFileOpen->GetOptions(&dwFlags)))
{
pFileOpen->SetOptions(dwFlags | FOS_PICKFOLDERS);
}
std::wstring wtitle(title.begin(), title.end());
pFileOpen->SetTitle(wtitle.c_str());
if (!initial_path.empty() && std::filesystem::exists(initial_path))
{
IShellItem *psi = NULL;
std::wstring winitial(initial_path.begin(), initial_path.end());
hr = SHCreateItemFromParsingName(winitial.c_str(), NULL, IID_IShellItem, (void **)&psi);
if (SUCCEEDED(hr))
{
pFileOpen->SetFolder(psi);
psi->Release();
}
}
hr = pFileOpen->Show(GetActiveWindow());
if (SUCCEEDED(hr))
{
IShellItem *pItem;
hr = pFileOpen->GetResult(&pItem);
if (SUCCEEDED(hr))
{
PWSTR pszFilePath;
hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath);
if (SUCCEEDED(hr))
{
std::wstring wpath(pszFilePath);
int size = WideCharToMultiByte(CP_UTF8, 0, wpath.c_str(), -1, nullptr, 0,
nullptr, nullptr);
std::string result(size - 1, 0);
WideCharToMultiByte(CP_UTF8, 0, wpath.c_str(), -1, &result[0], size, nullptr,
nullptr);
CoTaskMemFree(pszFilePath);
pItem->Release();
pFileOpen->Release();
return result;
}
pItem->Release();
}
}
pFileOpen->Release();
}
return "";
}
} // namespace file_dialogs
#endif

View File

@@ -0,0 +1,170 @@
#ifdef _WIN32
#include "core/config/config.hpp"
#include "gui/platform/font_loader.hpp"
#include "imgui_internal.h"
#include <algorithm>
#include <imgui.h>
#include <windows.h>
#include <winreg.h>
static std::string search_registry_for_font(HKEY root_key, const char *subkey,
const std::string &font_name_lower,
const char *default_font_dir)
{
HKEY hKey;
LONG result = RegOpenKeyExA(root_key, subkey, 0, KEY_READ, &hKey);
if (result != ERROR_SUCCESS)
return {};
char value_name[512];
BYTE value_data[512];
DWORD value_name_size, value_data_size, type;
DWORD index = 0;
std::string found_path;
while (true)
{
value_name_size = sizeof(value_name);
value_data_size = sizeof(value_data);
result = RegEnumValueA(hKey, index++, value_name, &value_name_size, nullptr, &type,
value_data, &value_data_size);
if (result != ERROR_SUCCESS)
break;
if (type != REG_SZ)
continue;
std::string reg_font_name = value_name;
std::transform(reg_font_name.begin(), reg_font_name.end(), reg_font_name.begin(),
::tolower);
std::string reg_font_name_clean = reg_font_name;
size_t type_pos = reg_font_name_clean.find(" (");
if (type_pos != std::string::npos)
reg_font_name_clean = reg_font_name_clean.substr(0, type_pos);
if (reg_font_name_clean == font_name_lower)
{
std::string font_file = reinterpret_cast<char *>(value_data);
// If path is not absolute, prepend default font directory
if (font_file.find(":\\") == std::string::npos)
{
found_path = std::string(default_font_dir) + "\\" + font_file;
}
else
{
found_path = font_file;
}
break;
}
}
RegCloseKey(hKey);
return found_path;
}
std::string font_loader::find_font_windows(const char *font_name)
{
std::string font_name_lower = font_name;
std::transform(font_name_lower.begin(), font_name_lower.end(), font_name_lower.begin(),
::tolower);
char windows_dir[MAX_PATH];
GetWindowsDirectoryA(windows_dir, MAX_PATH);
std::string system_fonts_dir = std::string(windows_dir) + "\\Fonts";
// First, try system-wide fonts (HKEY_LOCAL_MACHINE)
std::string path = search_registry_for_font(
HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts",
font_name_lower, system_fonts_dir.c_str());
if (!path.empty())
return path;
// If not found, try per-user fonts (HKEY_CURRENT_USER)
char local_appdata[MAX_PATH];
if (GetEnvironmentVariableA("LOCALAPPDATA", local_appdata, MAX_PATH) > 0)
{
std::string user_fonts_dir = std::string(local_appdata) + "\\Microsoft\\Windows\\Fonts";
path = search_registry_for_font(HKEY_CURRENT_USER,
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts",
font_name_lower, user_fonts_dir.c_str());
}
return path;
}
std::string font_loader::find_font_path(const char *font_name)
{
return find_font_windows(font_name);
}
ImFont *font_loader::load_font(const char *font_name, float size_px)
{
std::string path = find_font_path(font_name);
if (path.empty())
return nullptr;
float scale = ImGui::GetIO().DisplayFramebufferScale.y;
return ImGui::GetIO().Fonts->AddFontFromFileTTF(path.c_str(), size_px * scale);
}
void font_loader::push_default_font()
{
ImGui::PushFont(ImGui::GetDefaultFont(), clrsync::core::config::instance().font_size());
}
void font_loader::pop_font()
{
ImGui::PopFont();
}
std::vector<std::string> font_loader::get_system_fonts()
{
std::vector<std::string> fonts;
auto enumerate_registry_fonts = [&fonts](HKEY root_key, const char *subkey) {
HKEY hKey;
if (RegOpenKeyExA(root_key, subkey, 0, KEY_READ, &hKey) != ERROR_SUCCESS)
return;
char value_name[512];
DWORD value_name_size;
DWORD index = 0;
while (true)
{
value_name_size = sizeof(value_name);
LONG result = RegEnumValueA(hKey, index++, value_name, &value_name_size, nullptr,
nullptr, nullptr, nullptr);
if (result != ERROR_SUCCESS)
break;
std::string font_name = value_name;
size_t pos = font_name.find(" (");
if (pos != std::string::npos)
font_name = font_name.substr(0, pos);
if (std::find(fonts.begin(), fonts.end(), font_name) == fonts.end())
fonts.push_back(font_name);
}
RegCloseKey(hKey);
};
enumerate_registry_fonts(HKEY_LOCAL_MACHINE,
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts");
enumerate_registry_fonts(HKEY_CURRENT_USER,
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts");
std::sort(fonts.begin(), fonts.end());
return fonts;
}
#endif

View File

@@ -1,201 +0,0 @@
#include "settings_window.hpp"
#include "core/config/config.hpp"
#include "gui/font_loader.hpp"
#include "imgui.h"
#include <cstring>
settings_window::settings_window()
: m_font_size(14)
{
m_default_theme[0] = '\0';
m_palettes_path[0] = '\0';
m_font[0] = '\0';
load_settings();
}
void settings_window::render()
{
if (!m_visible)
return;
ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Settings", &m_visible))
{
ImGui::Text("General Settings");
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Default Theme:");
ImGui::SameLine();
ImGui::SetNextItemWidth(300.0f);
ImGui::InputText("##default_theme", m_default_theme, sizeof(m_default_theme));
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("The default color scheme to load on startup");
}
ImGui::Spacing();
ImGui::Text("Palettes Path:");
ImGui::SetNextItemWidth(-FLT_MIN);
ImGui::InputText("##palettes_path", m_palettes_path, sizeof(m_palettes_path));
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("Directory where color palettes are stored\nSupports ~ for home directory");
}
ImGui::Spacing();
ImGui::Text("Font:");
ImGui::SameLine();
ImGui::SetNextItemWidth(300.0f);
ImGui::InputText("##font", m_font, sizeof(m_font));
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("Font");
}
ImGui::Spacing();
ImGui::Text("Font Size:");
ImGui::SameLine();
ImGui::SetNextItemWidth(100.0f);
ImGui::InputInt("##font_size", &m_font_size, 1, 1);
if (m_font_size < 8) m_font_size = 8;
if (m_font_size > 48) m_font_size = 48;
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("Font size");
}
ImGui::Spacing();
ImGui::Spacing();
if (!m_error_message.empty())
{
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
ImGui::TextWrapped("%s", m_error_message.c_str());
ImGui::PopStyleColor();
ImGui::Spacing();
}
ImGui::Separator();
ImGui::Spacing();
if (ImGui::Button("OK", ImVec2(120, 0)))
{
apply_settings();
m_visible = false;
m_error_message = "";
}
ImGui::SameLine();
if (ImGui::Button("Apply", ImVec2(120, 0)))
{
apply_settings();
}
ImGui::SameLine();
if (ImGui::Button("Reset to Defaults", ImVec2(150, 0)))
{
strncpy(m_default_theme, "dark", sizeof(m_default_theme));
strncpy(m_palettes_path, "~/.config/clrsync/palettes", sizeof(m_palettes_path));
strncpy(m_font, "JetBrains Mono Nerd Font", sizeof(m_font));
m_font_size = 14;
m_error_message = "";
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(120, 0)))
{
load_settings();
m_visible = false;
m_error_message = "";
}
}
ImGui::End();
}
void settings_window::load_settings()
{
try
{
auto& cfg = clrsync::core::config::instance();
std::string default_theme = cfg.default_theme();
strncpy(m_default_theme, default_theme.c_str(), sizeof(m_default_theme) - 1);
m_default_theme[sizeof(m_default_theme) - 1] = '\0';
std::string palettes_path = cfg.palettes_path();
strncpy(m_palettes_path, palettes_path.c_str(), sizeof(m_palettes_path) - 1);
m_palettes_path[sizeof(m_palettes_path) - 1] = '\0';
std::string font = cfg.font();
strncpy(m_font, font.c_str(), sizeof(m_font) - 1);
m_font[sizeof(m_font) - 1] = '\0';
m_font_size = cfg.font_size();
m_error_message = "";
}
catch (const std::exception& e)
{
m_error_message = std::string("Failed to load settings: ") + e.what();
// Set defaults on error
strncpy(m_default_theme, "dark", sizeof(m_default_theme));
strncpy(m_palettes_path, "~/.config/clrsync/palettes", sizeof(m_palettes_path));
strncpy(m_font, "JetBrains Mono Nerd Font", sizeof(m_font));
m_font_size = 14;
}
}
void settings_window::apply_settings()
{
try
{
auto& cfg = clrsync::core::config::instance();
if (strlen(m_default_theme) == 0)
{
m_error_message = "Default theme cannot be empty";
return;
}
if (strlen(m_palettes_path) == 0)
{
m_error_message = "Palettes path cannot be empty";
return;
}
if (strlen(m_font) == 0)
{
m_error_message = "Font cannot be empty";
return;
}
if (m_font_size < 8 || m_font_size > 48)
{
m_error_message = "Font size must be between 8 and 48";
return;
}
cfg.set_default_theme(m_default_theme);
cfg.set_palettes_path(m_palettes_path);
cfg.set_font(m_font);
cfg.set_font_size(m_font_size);
font_loader fn_loader;
auto font = fn_loader.load_font(m_font, m_font_size);
if (font)
ImGui::GetIO().FontDefault = font;
m_error_message = "";
}
catch (const std::exception& e)
{
m_error_message = std::string("Failed to apply settings: ") + e.what();
}
}

View File

@@ -1,30 +0,0 @@
#ifndef CLRSYNC_GUI_SETTINGS_WINDOW_HPP
#define CLRSYNC_GUI_SETTINGS_WINDOW_HPP
#include <string>
class settings_window
{
public:
settings_window();
void render();
void show() { m_visible = true; }
void hide() { m_visible = false; }
bool is_visible() const { return m_visible; }
private:
void load_settings();
void save_settings();
void apply_settings();
bool m_visible{false};
char m_default_theme[128];
char m_palettes_path[512];
char m_font[128];
int m_font_size;
std::string m_error_message;
};
#endif // CLRSYNC_GUI_SETTINGS_WINDOW_HPP

View File

@@ -1,39 +0,0 @@
#include "template_controller.hpp"
#include "core/config/config.hpp"
template_controller::template_controller()
{
m_templates = m_template_manager.templates();
}
void template_controller::set_template_enabled(const std::string& key, bool enabled)
{
auto it = m_templates.find(key);
if (it != m_templates.end()) {
it->second.set_enabled(enabled);
clrsync::core::config::instance().update_template(key, it->second);
}
}
void template_controller::set_template_output_path(const std::string& key, const std::string& path)
{
auto it = m_templates.find(key);
if (it != m_templates.end()) {
it->second.set_output_path(path);
clrsync::core::config::instance().update_template(key, it->second);
}
}
void template_controller::set_template_reload_command(const std::string& key, const std::string& cmd)
{
auto it = m_templates.find(key);
if (it != m_templates.end()) {
it->second.set_reload_command(cmd);
clrsync::core::config::instance().update_template(key, it->second);
}
}
void template_controller::refresh()
{
m_templates = m_template_manager.templates();
}

View File

@@ -1,457 +0,0 @@
#include "template_editor.hpp"
#include "core/config/config.hpp"
#include "core/theme/theme_template.hpp"
#include "imgui.h"
#include <filesystem>
#include <fstream>
#include <ranges>
template_editor::template_editor() : m_template_name("new_template")
{
TextEditor::LanguageDefinition lang;
lang.mName = "Template";
lang.mCommentStart = "/*";
lang.mCommentEnd = "*/";
lang.mSingleLineComment = "#";
lang.mTokenRegexStrings.push_back(std::make_pair<std::string, TextEditor::PaletteIndex>(
"\\{[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)?\\}",
TextEditor::PaletteIndex::KnownIdentifier));
lang.mTokenRegexStrings.push_back(std::make_pair<std::string, TextEditor::PaletteIndex>(
"\"([^\"]*)\"", TextEditor::PaletteIndex::String));
lang.mTokenRegexStrings.push_back(std::make_pair<std::string, TextEditor::PaletteIndex>(
"'([^']*)'", TextEditor::PaletteIndex::String));
m_editor.SetLanguageDefinition(lang);
m_editor.SetText("# Enter your template here\n# Use {color_key} for color variables\n# "
"Examples: {color.hex}, {color.rgb}, {color.r}\n\n");
m_editor.SetShowWhitespaces(false);
}
void template_editor::apply_current_palette(const clrsync::core::palette &pal)
{
auto colors = pal.colors();
if (colors.empty())
return;
auto get_color_u32 = [&](const std::string &key, const std::string &fallback = "") -> uint32_t {
auto it = colors.find(key);
if (it == colors.end() && !fallback.empty())
{
it = colors.find(fallback);
}
if (it != colors.end())
{
const auto &col = it->second;
const uint32_t hex = col.hex();
// Convert from RRGGBBAA to AABBGGRR (ImGui format)
const uint32_t r = (hex >> 24) & 0xFF;
const uint32_t g = (hex >> 16) & 0xFF;
const uint32_t b = (hex >> 8) & 0xFF;
const uint32_t a = hex & 0xFF;
return (a << 24) | (b << 16) | (g << 8) | r;
}
return 0xFFFFFFFF;
};
auto palette = m_editor.GetPalette();
palette[int(TextEditor::PaletteIndex::Default)] = get_color_u32("editor_main", "foreground");
palette[int(TextEditor::PaletteIndex::Keyword)] = get_color_u32("editor_command", "accent");
palette[int(TextEditor::PaletteIndex::Number)] = get_color_u32("editor_warning", "warning");
palette[int(TextEditor::PaletteIndex::String)] = get_color_u32("editor_string", "success");
palette[int(TextEditor::PaletteIndex::CharLiteral)] = get_color_u32("editor_string", "success");
palette[int(TextEditor::PaletteIndex::Punctuation)] =
get_color_u32("editor_main", "foreground");
palette[int(TextEditor::PaletteIndex::Preprocessor)] =
get_color_u32("editor_emphasis", "accent");
palette[int(TextEditor::PaletteIndex::Identifier)] = get_color_u32("editor_main", "foreground");
palette[int(TextEditor::PaletteIndex::KnownIdentifier)] = get_color_u32("editor_link", "info");
palette[int(TextEditor::PaletteIndex::PreprocIdentifier)] =
get_color_u32("editor_link", "info");
palette[int(TextEditor::PaletteIndex::Comment)] =
get_color_u32("editor_comment", "editor_inactive");
palette[int(TextEditor::PaletteIndex::MultiLineComment)] =
get_color_u32("editor_comment", "editor_inactive");
palette[int(TextEditor::PaletteIndex::Background)] =
get_color_u32("editor_background", "background");
palette[int(TextEditor::PaletteIndex::Cursor)] = get_color_u32("cursor", "accent");
palette[int(TextEditor::PaletteIndex::Selection)] =
get_color_u32("editor_selected", "surface_variant");
palette[int(TextEditor::PaletteIndex::ErrorMarker)] = get_color_u32("editor_error", "error");
palette[int(TextEditor::PaletteIndex::Breakpoint)] = get_color_u32("editor_error", "error");
palette[int(TextEditor::PaletteIndex::LineNumber)] =
get_color_u32("editor_line_number", "editor_inactive");
palette[int(TextEditor::PaletteIndex::CurrentLineFill)] = get_color_u32("surface_variant");
palette[int(TextEditor::PaletteIndex::CurrentLineFillInactive)] = get_color_u32("surface");
palette[int(TextEditor::PaletteIndex::CurrentLineEdge)] =
get_color_u32("border_focused", "border");
m_editor.SetPalette(palette);
}
void template_editor::render()
{
ImGui::Begin("Templates");
render_controls();
ImGui::Separator();
const float panel_width = ImGui::GetContentRegionAvail().x;
constexpr float left_panel_width = 200.0f;
const float right_panel_width = panel_width - left_panel_width - 10;
ImGui::BeginChild("TemplateList", ImVec2(left_panel_width, 0), true);
render_template_list();
ImGui::EndChild();
ImGui::SameLine();
ImGui::BeginChild("EditorPanel", ImVec2(right_panel_width, 0), false);
render_editor();
ImGui::EndChild();
ImGui::End();
}
void template_editor::render_controls()
{
if (ImGui::Button("New Template"))
{
new_template();
}
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) &&
ImGui::IsKeyDown(ImGuiKey_LeftCtrl) && ImGui::IsKeyPressed(ImGuiKey_S))
{
save_template();
}
ImGui::SameLine();
if (ImGui::Button("Save"))
{
save_template();
}
ImGui::SameLine();
ImGui::Text("Template Name:");
ImGui::SameLine();
ImGui::SetNextItemWidth(150.0f);
char name_buf[256] = {0};
snprintf(name_buf, sizeof(name_buf), "%s", m_template_name.c_str());
if (ImGui::InputText("##template_name", name_buf, sizeof(name_buf)))
{
m_template_name = name_buf;
if (!m_template_name.empty())
{
m_validation_error = "";
}
}
ImGui::SameLine();
if (ImGui::Checkbox("Enabled", &m_enabled))
{
if (m_is_editing_existing)
{
m_template_controller.set_template_enabled(m_template_name, m_enabled);
}
}
ImGui::Text("Output Path:");
ImGui::SameLine();
ImGui::SetNextItemWidth(-FLT_MIN);
char path_buf[512] = {0};
snprintf(path_buf, sizeof(path_buf), "%s", m_output_path.c_str());
if (ImGui::InputText("##output_path", path_buf, sizeof(path_buf)))
{
m_output_path = path_buf;
if (!m_output_path.empty())
{
m_validation_error = "";
}
if (m_is_editing_existing)
{
m_template_controller.set_template_output_path(m_template_name, m_output_path);
}
}
ImGui::Text("Reload Command:");
ImGui::SameLine();
ImGui::SetNextItemWidth(-FLT_MIN);
char reload_buf[512] = {0};
snprintf(reload_buf, sizeof(reload_buf), "%s", m_reload_command.c_str());
if (ImGui::InputText("##reload_cmd", reload_buf, sizeof(reload_buf)))
{
m_reload_command = reload_buf;
if (m_is_editing_existing)
{
m_template_controller.set_template_reload_command(m_template_name, m_reload_command);
}
}
if (!m_validation_error.empty())
{
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
ImGui::TextWrapped("%s", m_validation_error.c_str());
ImGui::PopStyleColor();
}
}
void template_editor::render_editor()
{
if (!m_is_editing_existing)
{
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.8f, 0.4f, 1.0f));
ImGui::Text("New Template");
ImGui::PopStyleColor();
}
else
{
ImGui::Text("%s", m_template_name.c_str());
auto trim_right = [](const std::string &s) -> std::string {
size_t end = s.find_last_not_of("\r\n");
return (end == std::string::npos) ? "" : s.substr(0, end + 1);
};
std::string current_content = trim_right(m_editor.GetText());
std::string saved_content = trim_right(m_saved_content);
m_has_unsaved_changes = (current_content != saved_content);
if (m_has_unsaved_changes)
{
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.6f, 0.2f, 1.0f));
ImGui::Text("");
ImGui::PopStyleColor();
}
}
ImGui::Separator();
m_editor.Render("##TemplateEditor", ImVec2(0, 0), true);
}
void template_editor::render_template_list()
{
ImGui::Text("Templates");
ImGui::Separator();
if (!m_is_editing_existing)
{
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.8f, 0.4f, 1.0f));
ImGui::Selectable("* New Template *", true);
ImGui::PopStyleColor();
ImGui::Separator();
}
const auto &templates = m_template_controller.templates();
for (const auto &key : templates | std::views::keys)
{
const bool selected = (m_template_name == key && m_is_editing_existing);
if (ImGui::Selectable(key.c_str(), selected))
{
load_template(key);
}
}
}
bool template_editor::is_valid_path(const std::string &path)
{
const std::string invalid_chars = "<>|\"";
for (const char c : invalid_chars)
{
if (path.find(c) != std::string::npos)
{
return false;
}
}
if (path.find_first_not_of(" \t\n\r./\\") == std::string::npos)
{
return false;
}
try
{
std::filesystem::path fs_path(path);
const auto parent = fs_path.parent_path();
if (parent.empty())
{
return false;
}
if (!parent.empty() && !std::filesystem::exists(parent))
{
if (parent.string().find_first_of("<>|\"") != std::string::npos)
{
return false;
}
}
const auto filename = fs_path.filename().string();
if (filename.empty() || filename == "." || filename == "..")
{
return false;
}
return true;
}
catch (...)
{
return false;
}
}
void template_editor::save_template()
{
std::string trimmed_name = m_template_name;
trimmed_name.erase(0, trimmed_name.find_first_not_of(" \t\n\r"));
trimmed_name.erase(trimmed_name.find_last_not_of(" \t\n\r") + 1);
std::string trimmed_path = m_output_path;
trimmed_path.erase(0, trimmed_path.find_first_not_of(" \t\n\r"));
trimmed_path.erase(trimmed_path.find_last_not_of(" \t\n\r") + 1);
if (trimmed_name.empty())
{
m_validation_error = "Error: Template name cannot be empty!";
return;
}
if (trimmed_path.empty())
{
m_validation_error = "Error: Output path cannot be empty!";
return;
}
if (!is_valid_path(trimmed_path))
{
m_validation_error =
"Error: Output path is invalid! Must be a valid file path with directory.";
return;
}
m_validation_error = "";
try
{
auto &cfg = clrsync::core::config::instance();
std::string palettes_path = cfg.palettes_path();
std::filesystem::path templates_dir =
std::filesystem::path(palettes_path).parent_path() / "templates";
if (!std::filesystem::exists(templates_dir))
{
std::filesystem::create_directories(templates_dir);
}
std::filesystem::path template_file;
if (m_is_editing_existing)
{
const auto &existing_template = cfg.template_by_name(trimmed_name);
template_file = existing_template.template_path();
}
else
{
template_file = templates_dir / trimmed_name;
}
std::string template_content = m_editor.GetText();
std::ofstream out(template_file);
if (out.is_open())
{
out << template_content;
out.close();
}
clrsync::core::theme_template tmpl(trimmed_name, template_file.string(), trimmed_path);
tmpl.set_reload_command(m_reload_command);
tmpl.set_enabled(m_enabled);
cfg.update_template(trimmed_name, tmpl);
m_template_name = trimmed_name;
m_output_path = trimmed_path;
m_is_editing_existing = true;
m_saved_content = m_editor.GetText();
m_has_unsaved_changes = false;
refresh_templates();
}
catch (const std::exception &e)
{
m_validation_error = std::string("Error saving template: ") + e.what();
}
}
void template_editor::load_template(const std::string &name)
{
const auto &templates = m_template_controller.templates();
auto it = templates.find(name);
if (it != templates.end())
{
const auto &tmpl = it->second;
m_template_name = name;
m_output_path = tmpl.output_path();
m_reload_command = tmpl.reload_command();
m_enabled = tmpl.enabled();
m_is_editing_existing = true;
m_validation_error = "";
try
{
std::ifstream in(tmpl.template_path());
if (in.is_open())
{
std::string content;
std::string line;
while (std::getline(in, line))
{
content += line + "\n";
}
in.close();
m_editor.SetText(content);
m_saved_content = content;
m_has_unsaved_changes = false;
}
}
catch (const std::exception &e)
{
m_validation_error = std::string("Error loading template: ") + e.what();
}
}
}
void template_editor::new_template()
{
m_template_name = "new_template";
std::string default_content =
"# Enter your template here\n# Use {color_key} for color variables\n# "
"Examples: {color.hex}, {color.rgb}, {color.r}\n\n";
m_editor.SetText(default_content);
m_saved_content = default_content;
m_output_path = "";
m_reload_command = "";
m_enabled = true;
m_is_editing_existing = false;
m_validation_error = "";
m_has_unsaved_changes = false;
}
void template_editor::refresh_templates()
{
m_template_controller.refresh();
}

View File

@@ -1,42 +0,0 @@
#ifndef CLRSYNC_GUI_TEMPLATE_EDITOR_HPP
#define CLRSYNC_GUI_TEMPLATE_EDITOR_HPP
#include "template_controller.hpp"
#include <core/palette/palette.hpp>
#include "color_text_edit/TextEditor.h"
#include <string>
class template_editor
{
public:
template_editor();
void render();
void apply_current_palette(const clrsync::core::palette& pal);
private:
void render_controls();
void render_editor();
void render_template_list();
void save_template();
void load_template(const std::string &name);
void new_template();
void refresh_templates();
bool is_valid_path(const std::string &path);
template_controller m_template_controller;
TextEditor m_editor;
std::string m_template_name;
std::string m_output_path;
std::string m_reload_command;
std::string m_validation_error;
std::string m_saved_content;
bool m_has_unsaved_changes = false;
bool m_enabled{true};
bool m_is_editing_existing{false};
};
#endif // CLRSYNC_GUI_TEMPLATE_EDITOR_HPP

71
src/gui/ui_manager.cpp Normal file
View File

@@ -0,0 +1,71 @@
#include "gui/ui_manager.hpp"
#include "gui/backend/backend.hpp"
#include <imgui.h>
namespace clrsync::gui
{
ui_manager::ui_manager(backend::backend_interface* backend)
: m_backend(backend)
{
}
ui_manager::~ui_manager()
{
shutdown();
}
bool ui_manager::initialize(const ui_config& config)
{
IMGUI_CHECKVERSION();
m_imgui_context = ImGui::CreateContext();
if (!m_imgui_context)
{
return false;
}
ImGuiIO& io = ImGui::GetIO();
if (config.enable_docking)
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
if (config.enable_keyboard_nav)
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
if (!config.ini_path.empty())
io.IniFilename = config.ini_path.c_str();
ImGui::StyleColorsDark();
if (!m_backend->init_imgui_backend())
{
ImGui::DestroyContext(static_cast<ImGuiContext*>(m_imgui_context));
m_imgui_context = nullptr;
return false;
}
return true;
}
void ui_manager::shutdown()
{
if (m_imgui_context)
{
m_backend->shutdown_imgui_backend();
ImGui::DestroyContext(static_cast<ImGuiContext*>(m_imgui_context));
m_imgui_context = nullptr;
}
}
void ui_manager::begin_frame()
{
m_backend->imgui_new_frame();
ImGui::NewFrame();
}
void ui_manager::end_frame()
{
ImGui::Render();
m_backend->imgui_render_draw_data(ImGui::GetDrawData());
}
}

38
src/gui/ui_manager.hpp Normal file
View File

@@ -0,0 +1,38 @@
#ifndef CLRSYNC_UI_MANAGER_HPP
#define CLRSYNC_UI_MANAGER_HPP
#include <string>
namespace clrsync::gui::backend
{
class backend_interface;
}
namespace clrsync::gui
{
struct ui_config
{
std::string ini_path = "";
bool enable_docking = true;
bool enable_keyboard_nav = true;
};
class ui_manager
{
public:
explicit ui_manager(backend::backend_interface *backend);
~ui_manager();
bool initialize(const ui_config& config = ui_config());
void shutdown();
void begin_frame();
void end_frame();
private:
backend::backend_interface *m_backend;
void *m_imgui_context = nullptr;
};
}
#endif

View File

@@ -1,12 +1,13 @@
#include "about_window.hpp" #include "gui/views/about_window.hpp"
#include "core/version.hpp" #include "core/common/version.hpp"
#include "gui/helpers/imgui_helpers.hpp"
#include "imgui.h" #include "imgui.h"
about_window::about_window() about_window::about_window()
{ {
} }
void about_window::render() void about_window::render(const clrsync::core::palette &pal)
{ {
if (!m_visible) if (!m_visible)
return; return;
@@ -21,21 +22,21 @@ void about_window::render()
const char *title = "clrsync"; const char *title = "clrsync";
const float title_size = ImGui::CalcTextSize(title).x; const float title_size = ImGui::CalcTextSize(title).x;
ImGui::SetCursorPosX((window_width - title_size) * 0.5f); ImGui::SetCursorPosX((window_width - title_size) * 0.5f);
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "%s", title); ImVec4 title_color = palette_utils::get_color(pal, "info", "accent");
ImGui::TextColored(title_color, "%s", title);
ImGui::PopFont(); ImGui::PopFont();
std::string version = "Version " + clrsync::core::version_string(); std::string version = "Version " + clrsync::core::version_string();
const float version_size = ImGui::CalcTextSize(version.c_str()).x; const float version_size = ImGui::CalcTextSize(version.c_str()).x;
ImGui::SetCursorPosX((window_width - version_size) * 0.5f); ImGui::SetCursorPosX((window_width - version_size) * 0.5f);
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", version.c_str()); ImVec4 subtitle_color = palette_utils::get_color(pal, "editor_inactive", "foreground");
ImGui::TextColored(subtitle_color, "%s", version.c_str());
ImGui::Spacing(); ImGui::Spacing();
ImGui::Separator(); ImGui::Separator();
ImGui::Spacing(); ImGui::Spacing();
ImGui::TextWrapped( ImGui::TextWrapped("A color scheme management tool.");
"A color scheme management tool."
);
ImGui::Spacing(); ImGui::Spacing();
ImGui::Spacing(); ImGui::Spacing();
@@ -45,7 +46,12 @@ void about_window::render()
ImGui::Text("Links:"); ImGui::Text("Links:");
if (ImGui::Button("GitHub Repository", ImVec2(200, 0))) const float button_width = 200.0f;
const float spacing = ImGui::GetStyle().ItemSpacing.x;
const float total_width = 2.0f * button_width + spacing;
ImGui::SetCursorPosX((window_width - total_width) * 0.5f);
if (ImGui::Button("GitHub Repository", ImVec2(button_width, 0)))
{ {
#ifdef _WIN32 #ifdef _WIN32
system("start https://github.com/obsqrbtz/clrsync"); system("start https://github.com/obsqrbtz/clrsync");
@@ -57,12 +63,24 @@ void about_window::render()
} }
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button("Documentation", ImVec2(button_width, 0)))
{
#ifdef _WIN32
system("start https://binarygoose.dev/projects/clrsync/overview/");
#elif __APPLE__
system("open https://binarygoose.dev/projects/clrsync/overview/");
#else
system("xdg-open https://binarygoose.dev/projects/clrsync/overview/");
#endif
}
ImGui::Spacing(); ImGui::Spacing();
ImGui::Spacing(); ImGui::Spacing();
ImGui::Separator(); ImGui::Separator();
ImGui::Spacing(); ImGui::Spacing();
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "MIT License"); ImVec4 license_color = palette_utils::get_color(pal, "editor_inactive", "foreground");
ImGui::TextColored(license_color, "MIT License");
ImGui::TextWrapped( ImGui::TextWrapped(
"Copyright (c) 2025 Daniel Dada\n\n" "Copyright (c) 2025 Daniel Dada\n\n"
"Permission is hereby granted, free of charge, to any person obtaining a copy " "Permission is hereby granted, free of charge, to any person obtaining a copy "
@@ -72,18 +90,7 @@ void about_window::render()
"copies of the Software, and to permit persons to whom the Software is " "copies of the Software, and to permit persons to whom the Software is "
"furnished to do so, subject to the following conditions:\n\n" "furnished to do so, subject to the following conditions:\n\n"
"The above copyright notice and this permission notice shall be included in all " "The above copyright notice and this permission notice shall be included in all "
"copies or substantial portions of the Software." "copies or substantial portions of the Software.");
);
ImGui::Spacing();
ImGui::Spacing();
const float button_width = 120.0f;
ImGui::SetCursorPosX((window_width - button_width) * 0.5f);
if (ImGui::Button("Close", ImVec2(button_width, 0)))
{
m_visible = false;
}
} }
ImGui::End(); ImGui::End();
} }

View File

@@ -0,0 +1,33 @@
#ifndef CLRSYNC_GUI_ABOUT_WINDOW_HPP
#define CLRSYNC_GUI_ABOUT_WINDOW_HPP
#include "core/palette/palette.hpp"
class about_window
{
public:
about_window();
void render(const clrsync::core::palette &pal);
void render()
{
render(m_default_palette);
}
void show()
{
m_visible = true;
}
void hide()
{
m_visible = false;
}
bool is_visible() const
{
return m_visible;
}
private:
bool m_visible{false};
clrsync::core::palette m_default_palette;
};
#endif // CLRSYNC_GUI_ABOUT_WINDOW_HPP

View File

@@ -0,0 +1,201 @@
#include "color_scheme_editor.hpp"
#include "gui/controllers/theme_applier.hpp"
#include "gui/helpers/imgui_helpers.hpp"
#include "imgui.h"
#include "settings_window.hpp"
#include "template_editor.hpp"
#include <iostream>
#include <ranges>
color_scheme_editor::color_scheme_editor()
{
const auto &current = m_controller.current_palette();
if (!current.colors().empty())
{
theme_applier::apply_to_imgui(current);
m_preview.apply_palette(current);
}
else
{
std::cout << "WARNING: No palette loaded, skipping theme application\n";
}
}
void color_scheme_editor::notify_palette_changed()
{
if (m_template_editor)
{
m_template_editor->apply_current_palette(m_controller.current_palette());
}
if (m_settings_window)
{
m_settings_window->set_palette(m_controller.current_palette());
}
}
void color_scheme_editor::apply_themes()
{
const auto &current = m_controller.current_palette();
theme_applier::apply_to_imgui(current);
m_preview.apply_palette(current);
notify_palette_changed();
}
void color_scheme_editor::render_controls_and_colors()
{
ImGui::Begin("Color Schemes");
render_controls();
ImGui::Separator();
ImGui::BeginChild("ColorTableContent", ImVec2(0, 0), false);
m_color_table.render(m_controller.current_palette(), m_controller,
[this]() { apply_themes(); });
ImGui::EndChild();
ImGui::End();
}
void color_scheme_editor::render_preview()
{
ImGui::Begin("Color Preview");
m_preview.render(m_controller.current_palette());
ImGui::End();
}
void color_scheme_editor::render_controls()
{
const auto &current = m_controller.current_palette();
const auto &palettes = m_controller.palettes();
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6, 8));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8, 5));
ImGui::AlignTextToFramePadding();
ImGui::Text("Palette:");
ImGui::SameLine();
ImGui::SetNextItemWidth(200.0f);
if (ImGui::BeginCombo("##scheme", current.name().c_str()))
{
for (const auto &name : palettes | std::views::keys)
{
const bool selected = current.name() == name;
if (ImGui::Selectable(name.c_str(), selected))
{
m_controller.select_palette(name);
apply_themes();
}
if (selected)
ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Select a color palette to edit");
ImGui::SameLine();
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 8);
static char new_palette_name_buf[128] = "";
if (ImGui::Button(" + New "))
{
new_palette_name_buf[0] = 0;
ImGui::OpenPopup("New Palette");
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Create a new palette");
if (ImGui::BeginPopupModal("New Palette", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::Text("Enter a name for the new palette:");
ImGui::Spacing();
ImGui::SetNextItemWidth(250);
ImGui::InputTextWithHint("##new_palette_input", "Palette name...", new_palette_name_buf,
IM_ARRAYSIZE(new_palette_name_buf));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
bool can_create = strlen(new_palette_name_buf) > 0;
if (!can_create)
ImGui::BeginDisabled();
if (ImGui::Button("Create", ImVec2(120, 0)))
{
m_controller.create_palette(new_palette_name_buf);
m_controller.select_palette(new_palette_name_buf);
apply_themes();
new_palette_name_buf[0] = 0;
ImGui::CloseCurrentPopup();
}
if (!can_create)
ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(120, 0)))
{
new_palette_name_buf[0] = 0;
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
ImGui::SameLine();
if (ImGui::Button(" Save "))
{
m_controller.save_current_palette();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Save current palette to file");
ImGui::SameLine();
auto error = palette_utils::get_color(current, "error");
auto error_hover = ImVec4(error.x * 1.1f, error.y * 1.1f, error.z * 1.1f, error.w);
auto error_active = ImVec4(error.x * 0.8f, error.y * 0.8f, error.z * 0.8f, error.w);
auto on_error = palette_utils::get_color(current, "on_error");
ImGui::PushStyleColor(ImGuiCol_Button, error);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, error_hover);
ImGui::PushStyleColor(ImGuiCol_ButtonActive, error_active);
ImGui::PushStyleColor(ImGuiCol_Text, on_error);
if (ImGui::Button(" Delete "))
{
m_show_delete_confirmation = true;
}
ImGui::PopStyleColor(4);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Delete current palette");
if (m_show_delete_confirmation)
{
ImGui::OpenPopup("Delete Palette?");
m_show_delete_confirmation = false;
}
palette_utils::render_delete_confirmation_popup("Delete Palette?", current.name(), "palette",
current, [this]() {
m_controller.delete_current_palette();
apply_themes();
});
ImGui::SameLine();
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 16);
if (ImGui::Button(" Apply Theme "))
{
m_controller.apply_current_theme();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Apply current palette to all enabled templates");
ImGui::PopStyleVar(2);
}

View File

@@ -0,0 +1,44 @@
#ifndef CLRSYNC_GUI_COLOR_SCHEME_EDITOR_HPP
#define CLRSYNC_GUI_COLOR_SCHEME_EDITOR_HPP
#include "gui/controllers/palette_controller.hpp"
#include "gui/views/color_table_renderer.hpp"
#include "gui/views/preview_renderer.hpp"
class template_editor;
class settings_window;
class color_scheme_editor
{
public:
color_scheme_editor();
void render_controls_and_colors();
void render_preview();
void set_template_editor(template_editor *editor)
{
m_template_editor = editor;
}
void set_settings_window(settings_window *window)
{
m_settings_window = window;
}
const palette_controller &controller() const
{
return m_controller;
}
private:
void render_controls();
void apply_themes();
void notify_palette_changed();
palette_controller m_controller;
color_table_renderer m_color_table;
preview_renderer m_preview;
template_editor *m_template_editor{nullptr};
settings_window *m_settings_window{nullptr};
bool m_show_delete_confirmation{false};
};
#endif // CLRSYNC_GUI_COLOR_SCHEME_EDITOR_HPP

View File

@@ -0,0 +1,202 @@
#include "gui/views/color_table_renderer.hpp"
#include "gui/helpers/imgui_helpers.hpp"
#include "imgui.h"
#include <algorithm>
#include <cctype>
#include <vector>
bool color_table_renderer::matches_filter(const std::string &name) const
{
if (m_filter_text[0] == '\0')
return true;
std::string filter_lower = m_filter_text;
std::string name_lower = name;
std::transform(filter_lower.begin(), filter_lower.end(), filter_lower.begin(),
[](unsigned char c) { return std::tolower(c); });
std::transform(name_lower.begin(), name_lower.end(), name_lower.begin(),
[](unsigned char c) { return std::tolower(c); });
return name_lower.find(filter_lower) != std::string::npos;
}
void color_table_renderer::render_color_row(const std::string &name,
const clrsync::core::palette &current,
palette_controller &controller,
const OnColorChangedCallback &on_changed)
{
if (!matches_filter(name))
return;
const clrsync::core::color &col = current.get_color(name);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
const float key_col_width = ImGui::GetContentRegionAvail().x;
ImVec4 text_color = palette_utils::get_color(current, "info", "accent");
ImGui::PushStyleColor(ImGuiCol_Text, text_color);
const bool copied = ImGui::Selectable(name.c_str(), false, 0, ImVec2(key_col_width, 0.0f));
ImGui::PopStyleColor();
if (ImGui::IsItemHovered())
{
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Click to copy: {%s.hex}", name.c_str());
}
if (copied)
{
std::string template_var = "{" + name + ".hex}";
ImGui::SetClipboardText(template_var.c_str());
}
ImGui::TableSetColumnIndex(1);
{
std::string hex_str = col.to_hex_string();
char buf[9];
strncpy(buf, hex_str.c_str(), sizeof(buf));
buf[8] = 0;
ImGui::SetNextItemWidth(-FLT_MIN);
if (ImGui::InputText(("##hex_" + name).c_str(), buf, sizeof(buf),
ImGuiInputTextFlags_CharsUppercase))
{
try
{
clrsync::core::color new_color;
new_color.from_hex_string(buf);
controller.set_color(name, new_color);
if (on_changed)
on_changed();
}
catch (...)
{
}
}
}
ImGui::TableSetColumnIndex(2);
ImGui::PushID(name.c_str());
float c[4] = {((col.hex() >> 24) & 0xFF) / 255.0f, ((col.hex() >> 16) & 0xFF) / 255.0f,
((col.hex() >> 8) & 0xFF) / 255.0f, (col.hex() & 0xFF) / 255.0f};
ImGui::SetNextItemWidth(-FLT_MIN);
if (ImGui::ColorEdit4(("##color_" + name).c_str(), c,
ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel |
ImGuiColorEditFlags_AlphaBar | ImGuiColorEditFlags_AlphaPreviewHalf))
{
uint32_t r = (uint32_t)(c[0] * 255.0f);
uint32_t g = (uint32_t)(c[1] * 255.0f);
uint32_t b = (uint32_t)(c[2] * 255.0f);
uint32_t a = (uint32_t)(c[3] * 255.0f);
uint32_t hex = (r << 24) | (g << 16) | (b << 8) | a;
controller.set_color(name, clrsync::core::color(hex));
if (on_changed)
on_changed();
}
ImGui::PopID();
}
void color_table_renderer::render(const clrsync::core::palette &current,
palette_controller &controller,
const OnColorChangedCallback &on_changed)
{
if (current.colors().empty())
{
ImVec4 warning_color = palette_utils::get_color(current, "warning", "accent");
ImGui::TextColored(warning_color, "No palette loaded");
return;
}
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8, 6));
ImGui::Text("Filter:");
ImGui::SameLine();
ImGui::SetNextItemWidth(200);
bool filter_changed = ImGui::InputTextWithHint("##color_filter", "Search colors...",
m_filter_text, sizeof(m_filter_text));
if (m_filter_text[0] != '\0')
{
ImGui::SameLine();
if (ImGui::SmallButton("X"))
{
m_filter_text[0] = '\0';
filter_changed = true;
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Clear filter");
}
ImGui::PopStyleVar();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
auto draw_table = [&](const char *title, const char *id,
const std::vector<const char *> &keys) {
bool has_matches = false;
for (auto *k : keys)
{
if (matches_filter(k))
{
has_matches = true;
break;
}
}
if (!has_matches)
return;
ImGui::PushStyleColor(ImGuiCol_Text, palette_utils::get_color(current, "accent"));
bool header_open = ImGui::TreeNodeEx(title, ImGuiTreeNodeFlags_DefaultOpen |
ImGuiTreeNodeFlags_SpanAvailWidth);
ImGui::PopStyleColor();
if (header_open)
{
if (ImGui::BeginTable(id, 3,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
ImGuiTableFlags_SizingStretchProp))
{
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 160.0f);
ImGui::TableSetupColumn("HEX", ImGuiTableColumnFlags_WidthFixed, 95.0f);
ImGui::TableSetupColumn("Color", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableHeadersRow();
for (auto *k : keys)
render_color_row(k, current, controller, on_changed);
ImGui::EndTable();
}
ImGui::TreePop();
}
ImGui::Spacing();
};
draw_table("General UI", "##general_ui",
{"background", "on_background", "surface", "on_surface", "surface_variant",
"on_surface_variant", "foreground", "cursor", "accent"});
draw_table("Borders", "##borders", {"border_focused", "border"});
draw_table(
"Semantic Colors", "##semantic",
{"success", "info", "warning", "error", "on_success", "on_info", "on_warning", "on_error"});
draw_table("Editor", "##editor",
{"editor_background", "editor_command", "editor_comment", "editor_disabled",
"editor_emphasis", "editor_error", "editor_inactive", "editor_line_number",
"editor_link", "editor_main", "editor_selected", "editor_selection_inactive",
"editor_string", "editor_success", "editor_warning"});
draw_table("Terminal (Base16)", "##terminal",
{"base00", "base01", "base02", "base03", "base04", "base05", "base06", "base07",
"base08", "base09", "base0A", "base0B", "base0C", "base0D", "base0E", "base0F"});
}

View File

@@ -0,0 +1,27 @@
#ifndef CLRSYNC_GUI_COLOR_TABLE_RENDERER_HPP
#define CLRSYNC_GUI_COLOR_TABLE_RENDERER_HPP
#include "core/palette/palette.hpp"
#include "gui/controllers/palette_controller.hpp"
#include <functional>
#include <string>
class color_table_renderer
{
public:
using OnColorChangedCallback = std::function<void()>;
void render(const clrsync::core::palette &palette, palette_controller &controller,
const OnColorChangedCallback &on_changed);
private:
void render_color_row(const std::string &name, const clrsync::core::palette &palette,
palette_controller &controller, const OnColorChangedCallback &on_changed);
bool matches_filter(const std::string &name) const;
char m_filter_text[128] = {0};
bool m_show_only_modified{false};
};
#endif // CLRSYNC_GUI_COLOR_TABLE_RENDERER_HPP

View File

@@ -0,0 +1,246 @@
#include "gui/views/preview_renderer.hpp"
#include "gui/controllers/theme_applier.hpp"
#include "gui/helpers/imgui_helpers.hpp"
#include "imgui.h"
#include <algorithm>
#include <array>
preview_renderer::preview_renderer()
{
m_editor.SetLanguageDefinition(TextEditor::LanguageDefinition::CPlusPlus());
m_editor.SetText(R"(#include <iostream>
#include <string>
#include <vector>
#include <filesystem>
#include <cstdlib>
namespace fs = std::filesystem;
std::string expand_user(const std::string &path)
{
if (path.empty()) return "";
std::string result;
if (path[0] == '~')
{
#ifdef _WIN32
const char* home = std::getenv("USERPROFILE");
#else
const char* home = std::getenv("HOME");
#endif
result = home ? std::string(home) : "~";
result += path.substr(1);
}
else
{
result = path;
}
return result;
}
std::vector<std::string> list_files(const std::string &dir_path)
{
std::vector<std::string> files;
try
{
for (const auto &entry : fs::directory_iterator(dir_path))
{
if (entry.is_regular_file())
files.push_back(entry.path().string());
}
}
catch (const std::exception &e)
{
std::cerr << "Error: " << e.what() << std::endl;
}
return files;
}
int main()
{
std::string path = expand_user("~/Documents");
std::cout << "Listing files in: " << path << std::endl;
auto files = list_files(path);
for (const auto &f : files)
std::cout << " " << f << std::endl;
return 0;
})");
m_editor.SetShowWhitespaces(false);
}
static ImVec4 hex_to_imvec4(uint32_t hex)
{
return {((hex >> 24) & 0xFF) / 255.0f, ((hex >> 16) & 0xFF) / 255.0f,
((hex >> 8) & 0xFF) / 255.0f, (hex & 0xFF) / 255.0f};
}
void preview_renderer::apply_palette(const clrsync::core::palette &palette)
{
theme_applier::apply_to_editor(m_editor, palette);
}
void preview_renderer::render_code_preview()
{
const float avail_height = ImGui::GetContentRegionAvail().y;
const float code_preview_height = std::max(250.0f, avail_height * 0.50f);
ImGui::Text("Code Editor Preview:");
ImGui::SameLine();
ImGui::TextDisabled("(editor_* colors)");
m_editor.Render("##CodeEditor", ImVec2(0, code_preview_height), true);
}
void preview_renderer::render_terminal_preview(const clrsync::core::palette &current)
{
auto get_color = [&](const std::string &key) -> ImVec4 {
const auto &col = current.get_color(key);
return hex_to_imvec4(col.hex());
};
const ImVec4 bg = get_color("base00");
const ImVec4 fg = get_color("base07");
const ImVec4 cursor_col = get_color("cursor");
const ImVec4 border_col = get_color("border");
const ImVec4 black = get_color("base00");
const ImVec4 red = get_color("base01");
const ImVec4 green = get_color("base02");
const ImVec4 yellow = get_color("base03");
const ImVec4 blue = get_color("base04");
const ImVec4 magenta = get_color("base05");
const ImVec4 cyan = get_color("base06");
const ImVec4 white = get_color("base07");
const ImVec4 bright_black = get_color("base08");
const ImVec4 bright_red = get_color("base09");
const ImVec4 bright_green = get_color("base0A");
const ImVec4 bright_yellow = get_color("base0B");
const ImVec4 bright_blue = get_color("base0C");
const ImVec4 bright_magenta = get_color("base0D");
const ImVec4 bright_cyan = get_color("base0E");
const ImVec4 bright_white = get_color("base0F");
ImGui::Spacing();
ImGui::Text("Terminal Preview:");
ImGui::SameLine();
ImGui::TextDisabled("(base00-base0F colors)");
ImGui::PushStyleColor(ImGuiCol_ChildBg, bg);
ImGui::PushStyleColor(ImGuiCol_Border, border_col);
const float terminal_height = std::max(200.0f, ImGui::GetContentRegionAvail().y - 10.0f);
ImGui::BeginChild("TerminalPreview", ImVec2(0, terminal_height), true);
ImGui::TextColored(green, "user@host");
ImGui::SameLine(0, 0);
ImGui::TextColored(fg, ":");
ImGui::SameLine(0, 0);
ImGui::TextColored(blue, "~/projects");
ImGui::SameLine(0, 0);
ImGui::TextColored(fg, "$ ");
ImGui::SameLine(0, 0);
ImGui::TextColored(fg, "ls -la");
ImGui::TextColored(fg, "total 48");
ImGui::TextColored(blue, "drwxr-xr-x");
ImGui::SameLine();
ImGui::TextColored(fg, " 5 user group 4096 Dec 2 10:30 ");
ImGui::SameLine(0, 0);
ImGui::TextColored(blue, ".");
ImGui::TextColored(blue, "drwxr-xr-x");
ImGui::SameLine();
ImGui::TextColored(fg, " 3 user group 4096 Dec 1 09:15 ");
ImGui::SameLine(0, 0);
ImGui::TextColored(blue, "..");
ImGui::TextColored(fg, "-rw-r--r--");
ImGui::SameLine();
ImGui::TextColored(fg, " 1 user group 1234 Dec 2 10:30 ");
ImGui::SameLine(0, 0);
ImGui::TextColored(fg, "README.md");
ImGui::TextColored(fg, "-rwxr-xr-x");
ImGui::SameLine();
ImGui::TextColored(fg, " 1 user group 8192 Dec 2 10:28 ");
ImGui::SameLine(0, 0);
ImGui::TextColored(green, "build.sh");
ImGui::TextColored(cyan, "lrwxrwxrwx");
ImGui::SameLine();
ImGui::TextColored(fg, " 1 user group 24 Dec 1 15:00 ");
ImGui::SameLine(0, 0);
ImGui::TextColored(cyan, "config -> ~/.config/app");
ImGui::Spacing();
ImGui::TextColored(green, "user@host");
ImGui::SameLine(0, 0);
ImGui::TextColored(fg, ":");
ImGui::SameLine(0, 0);
ImGui::TextColored(blue, "~/projects");
ImGui::SameLine(0, 0);
ImGui::TextColored(fg, "$ ");
ImGui::SameLine(0, 0);
ImGui::TextColored(fg, "git status");
ImGui::TextColored(fg, "On branch ");
ImGui::SameLine(0, 0);
ImGui::TextColored(green, "main");
ImGui::TextColored(fg, "Changes to be committed:");
ImGui::TextColored(green, " modified: src/main.cpp");
ImGui::TextColored(green, " new file: src/utils.hpp");
ImGui::TextColored(fg, "Changes not staged:");
ImGui::TextColored(red, " modified: README.md");
ImGui::Spacing();
ImGui::TextColored(fg, "ANSI Colors (0-7 / 8-15):");
const float box_size = 20.0f;
const float spacing = 4.0f;
ImVec2 start_pos = ImGui::GetCursorScreenPos();
ImDrawList *draw_list = ImGui::GetWindowDrawList();
std::array<ImVec4, 8> normal_colors = {black, red, green, yellow, blue, magenta, cyan, white};
for (size_t i = 0; i < 8; i++)
{
ImVec2 p0 = ImVec2(start_pos.x + i * (box_size + spacing), start_pos.y);
ImVec2 p1 = ImVec2(p0.x + box_size, p0.y + box_size);
draw_list->AddRectFilled(p0, p1, ImGui::ColorConvertFloat4ToU32(normal_colors[i]));
draw_list->AddRect(p0, p1, ImGui::ColorConvertFloat4ToU32(border_col));
}
std::array<ImVec4, 8> bright_colors = {bright_black, bright_red, bright_green,
bright_yellow, bright_blue, bright_magenta,
bright_cyan, bright_white};
for (size_t i = 0; i < 8; i++)
{
ImVec2 p0 =
ImVec2(start_pos.x + i * (box_size + spacing), start_pos.y + box_size + spacing);
ImVec2 p1 = ImVec2(p0.x + box_size, p0.y + box_size);
draw_list->AddRectFilled(p0, p1, ImGui::ColorConvertFloat4ToU32(bright_colors[i]));
draw_list->AddRect(p0, p1, ImGui::ColorConvertFloat4ToU32(border_col));
}
ImGui::Dummy(ImVec2(8 * (box_size + spacing), 2 * box_size + spacing + 4));
ImGui::PopStyleColor(2);
ImGui::EndChild();
}
void preview_renderer::render(const clrsync::core::palette &current)
{
if (current.colors().empty())
{
ImVec4 error_color = palette_utils::get_color(current, "error", "accent");
ImGui::TextColored(error_color, "Current palette is empty");
return;
}
render_code_preview();
render_terminal_preview(current);
}

View File

@@ -0,0 +1,22 @@
#ifndef CLRSYNC_GUI_PREVIEW_RENDERER_HPP
#define CLRSYNC_GUI_PREVIEW_RENDERER_HPP
#include "color_text_edit/TextEditor.h"
#include "core/palette/palette.hpp"
class preview_renderer
{
public:
preview_renderer();
void render(const clrsync::core::palette &palette);
void apply_palette(const clrsync::core::palette &palette);
private:
void render_code_preview();
void render_terminal_preview(const clrsync::core::palette &palette);
TextEditor m_editor;
};
#endif // CLRSYNC_GUI_PREVIEW_RENDERER_HPP

View File

@@ -0,0 +1,378 @@
#include "gui/views/settings_window.hpp"
#include "core/common/error.hpp"
#include "core/config/config.hpp"
#include "gui/helpers/imgui_helpers.hpp"
#include "gui/platform/file_browser.hpp"
#include "gui/platform/font_loader.hpp"
#include "imgui.h"
#include <cstring>
settings_window::settings_window()
: m_font_size(14), m_selected_font_idx(0), m_settings_changed(false), m_current_tab(0)
{
m_default_theme[0] = '\0';
m_palettes_path[0] = '\0';
m_font[0] = '\0';
font_loader loader;
m_available_fonts = loader.get_system_fonts();
load_settings();
}
void settings_window::render()
{
if (!m_visible)
return;
ImGui::SetNextWindowSize(ImVec2(700, 500), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_FirstUseEver,
ImVec2(0.5f, 0.5f));
ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoCollapse;
if (ImGui::Begin("Settings", &m_visible, window_flags))
{
if (ImGui::BeginTabBar("SettingsTabs", ImGuiTabBarFlags_None))
{
if (ImGui::BeginTabItem("General"))
{
render_general_tab();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Appearance"))
{
render_appearance_tab();
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
render_status_messages();
ImGui::Separator();
render_action_buttons();
}
ImGui::End();
}
void settings_window::load_settings()
{
auto &cfg = clrsync::core::config::instance();
std::string default_theme = cfg.default_theme();
strncpy(m_default_theme, default_theme.c_str(), sizeof(m_default_theme) - 1);
m_default_theme[sizeof(m_default_theme) - 1] = '\0';
std::string palettes_path = cfg.palettes_path();
strncpy(m_palettes_path, palettes_path.c_str(), sizeof(m_palettes_path) - 1);
m_palettes_path[sizeof(m_palettes_path) - 1] = '\0';
std::string font = cfg.font();
strncpy(m_font, font.c_str(), sizeof(m_font) - 1);
m_font[sizeof(m_font) - 1] = '\0';
m_selected_font_idx = 0;
for (int i = 0; i < static_cast<int>(m_available_fonts.size()); i++)
{
if (m_available_fonts[i] == font)
{
m_selected_font_idx = i;
break;
}
}
m_font_size = cfg.font_size();
m_error_message.clear();
m_settings_changed = false;
}
void settings_window::apply_settings()
{
auto &cfg = clrsync::core::config::instance();
if (strlen(m_default_theme) == 0)
{
m_error_message = "Default theme cannot be empty";
return;
}
if (strlen(m_palettes_path) == 0)
{
m_error_message = "Palettes path cannot be empty";
return;
}
if (strlen(m_font) == 0)
{
m_error_message = "Font cannot be empty";
return;
}
if (m_font_size < 8 || m_font_size > 48)
{
m_error_message = "Font size must be between 8 and 48";
return;
}
auto result1 = cfg.set_default_theme(m_default_theme);
if (!result1)
{
m_error_message = "Failed to set default theme: " + result1.error().description();
return;
}
auto result2 = cfg.set_palettes_path(m_palettes_path);
if (!result2)
{
m_error_message = "Failed to set palettes path: " + result2.error().description();
return;
}
auto result3 = cfg.set_font(m_font);
if (!result3)
{
m_error_message = "Failed to set font: " + result3.error().description();
return;
}
auto result4 = cfg.set_font_size(m_font_size);
if (!result4)
{
m_error_message = "Failed to set font size: " + result4.error().description();
return;
}
font_loader fn_loader;
auto font = fn_loader.load_font(m_font, m_font_size);
if (font)
ImGui::GetIO().FontDefault = font;
m_error_message.clear();
m_settings_changed = false;
}
void settings_window::render_general_tab()
{
ImGui::Spacing();
auto accent_color = palette_utils::get_color(m_current_palette, "accent");
ImGui::TextColored(accent_color, "Theme Settings");
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Default Theme:");
ImGui::SameLine();
show_help_marker("The default color scheme to load on startup");
ImGui::SetNextItemWidth(-100.0f);
if (ImGui::InputText("##default_theme", m_default_theme, sizeof(m_default_theme)))
m_settings_changed = true;
ImGui::Spacing();
ImGui::TextColored(accent_color, "Path Settings");
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Palettes Directory:");
ImGui::SameLine();
show_help_marker("Directory where color palettes are stored\nSupports ~ for home directory");
ImGui::SetNextItemWidth(-120.0f);
if (ImGui::InputText("##palettes_path", m_palettes_path, sizeof(m_palettes_path)))
m_settings_changed = true;
ImGui::SameLine();
if (ImGui::Button("Browse"))
{
std::string selected_path =
file_dialogs::select_folder_dialog("Select Palettes Directory", m_palettes_path);
if (!selected_path.empty())
{
strncpy(m_palettes_path, selected_path.c_str(), sizeof(m_palettes_path) - 1);
m_palettes_path[sizeof(m_palettes_path) - 1] = '\0';
m_settings_changed = true;
}
}
}
void settings_window::render_appearance_tab()
{
ImGui::Spacing();
auto accent_color = palette_utils::get_color(m_current_palette, "accent");
ImGui::TextColored(accent_color, "Font Settings");
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Font Family:");
ImGui::SameLine();
show_help_marker("Select font family for the application interface");
ImGui::SetNextItemWidth(-1.0f);
if (ImGui::BeginCombo("##font", m_font))
{
for (int i = 0; i < static_cast<int>(m_available_fonts.size()); i++)
{
bool is_selected = (i == m_selected_font_idx);
if (ImGui::Selectable(m_available_fonts[i].c_str(), is_selected))
{
m_selected_font_idx = i;
strncpy(m_font, m_available_fonts[i].c_str(), sizeof(m_font) - 1);
m_font[sizeof(m_font) - 1] = '\0';
m_settings_changed = true;
}
if (is_selected)
ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
ImGui::Spacing();
ImGui::Text("Font Size:");
ImGui::SameLine();
show_help_marker("Font size for the application interface (8-48)");
ImGui::SetNextItemWidth(120.0f);
int old_size = m_font_size;
if (ImGui::SliderInt("##font_size", &m_font_size, 8, 48, "%d px"))
{
if (old_size != m_font_size)
m_settings_changed = true;
}
ImGui::SameLine();
if (ImGui::Button("Reset"))
{
m_font_size = 14;
m_settings_changed = true;
}
}
void settings_window::render_status_messages()
{
if (!m_error_message.empty())
{
ImGui::Spacing();
auto error_bg_color = palette_utils::get_color(m_current_palette, "error");
auto error_text_color = palette_utils::get_color(m_current_palette, "on_error");
ImGui::PushStyleColor(ImGuiCol_ChildBg, error_bg_color);
ImGui::PushStyleColor(ImGuiCol_Border, error_bg_color);
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f);
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f);
if (ImGui::BeginChild("##error_box", ImVec2(0, 0),
ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_Borders))
{
ImGui::PushStyleColor(ImGuiCol_Text, error_text_color);
ImGui::TextWrapped("Error: %s", m_error_message.c_str());
ImGui::PopStyleColor();
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Button,
ImVec4(error_bg_color.x * 0.8f, error_bg_color.y * 0.8f,
error_bg_color.z * 0.8f, error_bg_color.w));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImVec4(error_bg_color.x * 0.6f, error_bg_color.y * 0.6f,
error_bg_color.z * 0.6f, error_bg_color.w));
ImGui::PushStyleColor(ImGuiCol_Text, error_text_color);
if (ImGui::Button("Dismiss##error"))
m_error_message.clear();
ImGui::PopStyleColor(3);
}
ImGui::EndChild();
ImGui::PopStyleVar(2);
ImGui::PopStyleColor(2);
}
}
void settings_window::render_action_buttons()
{
ImGui::Spacing();
float button_width = 100.0f;
float spacing = ImGui::GetStyle().ItemSpacing.x;
float window_width = ImGui::GetContentRegionAvail().x;
float total_buttons_width = 4 * button_width + 3 * spacing;
float start_pos = (window_width - total_buttons_width) * 0.5f;
if (start_pos > 0)
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + start_pos);
if (ImGui::Button("OK", ImVec2(button_width, 0)))
{
apply_settings();
if (m_error_message.empty())
{
m_visible = false;
m_settings_changed = false;
}
}
ImGui::SameLine();
bool apply_disabled = !m_settings_changed;
if (apply_disabled)
{
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f);
}
if (ImGui::Button("Apply", ImVec2(button_width, 0)) && !apply_disabled)
{
apply_settings();
if (m_error_message.empty())
{
m_settings_changed = false;
}
}
if (apply_disabled)
{
ImGui::PopStyleVar();
}
ImGui::SameLine();
if (ImGui::Button("Reset", ImVec2(button_width, 0)))
{
reset_to_defaults();
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(button_width, 0)))
{
load_settings();
m_visible = false;
m_error_message.clear();
m_settings_changed = false;
}
}
void settings_window::show_help_marker(const char *desc)
{
ImGui::TextDisabled("(?)");
if (ImGui::BeginItemTooltip())
{
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f);
ImGui::TextUnformatted(desc);
ImGui::PopTextWrapPos();
ImGui::EndTooltip();
}
}
void settings_window::reset_to_defaults()
{
strncpy(m_default_theme, "dark", sizeof(m_default_theme));
strncpy(m_palettes_path, "~/.config/clrsync/palettes", sizeof(m_palettes_path));
strncpy(m_font, "JetBrains Mono Nerd Font", sizeof(m_font));
m_font_size = 14;
m_error_message.clear();
m_settings_changed = true;
}

View File

@@ -0,0 +1,60 @@
#ifndef CLRSYNC_GUI_SETTINGS_WINDOW_HPP
#define CLRSYNC_GUI_SETTINGS_WINDOW_HPP
#include "core/palette/palette.hpp"
#include <string>
#include <vector>
class settings_window
{
public:
settings_window();
void render();
void show()
{
m_visible = true;
}
void hide()
{
m_visible = false;
}
bool is_visible() const
{
return m_visible;
}
private:
void load_settings();
void save_settings();
void apply_settings();
void render_general_tab();
void render_appearance_tab();
void render_status_messages();
void render_action_buttons();
void show_help_marker(const char *desc);
void reset_to_defaults();
public:
void set_palette(const clrsync::core::palette &palette)
{
m_current_palette = palette;
}
bool m_visible{false};
char m_default_theme[128];
char m_palettes_path[512];
char m_font[128];
int m_font_size;
std::vector<std::string> m_available_fonts;
int m_selected_font_idx;
std::string m_error_message;
bool m_settings_changed;
int m_current_tab;
clrsync::core::palette m_current_palette;
};
#endif // CLRSYNC_GUI_SETTINGS_WINDOW_HPP

Some files were not shown because too many files have changed in this diff Show More