diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index c6c38d7..a2fa39d 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -25,8 +25,7 @@ jobs: if: runner.os == 'Linux' run: | sudo apt-get update - sudo apt-get install -y build-essential cmake libgl1-mesa-dev libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libwayland-dev libxkbcommon-dev - + sudo apt-get install -y build-essential cmake libgl1-mesa-dev libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libwayland-dev libxkbcommon-dev libcurl4-openssl-dev - name: Configure and build (Linux) if: runner.os == 'Linux' run: | @@ -34,7 +33,6 @@ jobs: cd build cmake .. -DCMAKE_BUILD_TYPE=Release make - - name: Configure and build (Windows) if: runner.os == 'Windows' run: | diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 83ddd04..0cca86c 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -21,7 +21,7 @@ jobs: if: runner.os == 'Linux' run: | sudo apt-get update - sudo apt-get install -y build-essential cmake libgl1-mesa-dev libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libwayland-dev libxkbcommon-dev + sudo apt-get install -y build-essential cmake libgl1-mesa-dev libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libwayland-dev libxkbcommon-dev libcurl4-openssl-dev - name: Configure and build (Linux) if: runner.os == 'Linux' diff --git a/CMakeLists.txt b/CMakeLists.txt index def8735..a90c977 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,21 @@ set(IMGUI_BACKEND_SRC external/imgui/backends/imgui_impl_opengl3.cpp ) +if(WIN32) + # fetch curl + include(FetchContent) + FetchContent_Declare( + curl + URL https://curl.se/download/curl-8.5.0.tar.gz + DOWNLOAD_EXTRACT_TIMESTAMP true + OVERRIDE_FIND_PACKAGE + ) + FetchContent_MakeAvailable(curl) +else() +# On Linux, we can use the system's libcurl + find_package(CURL REQUIRED) +endif() + # --- IMGUI --- add_library(imgui STATIC ${IMGUI_CORE_SRC} ${IMGUI_BACKEND_SRC}) target_include_directories(imgui PUBLIC external/imgui) @@ -58,9 +73,9 @@ target_include_directories(ui PUBLIC include external/imgui external/ImGuiFileDi target_link_libraries(ui PUBLIC core imgui glad glfw imguifiledialog imgui_gradient) # --- App library --- -add_library(app STATIC src/app.cpp) +add_library(app STATIC src/app.cpp src/update.cpp) target_include_directories(app PUBLIC include ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(app PUBLIC core cache ui imgui glad glfw hex_display_feature_manager) +target_link_libraries(app PUBLIC core cache ui imgui glad glfw hex_display_feature_manager CURL::libcurl) if(WIN32) add_executable(EntropyVisualizer WIN32 main.cpp) diff --git a/VERSION.txt b/VERSION.txt index 0bfbd57..f8e233b 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -1.8.2 \ No newline at end of file +1.9.0 diff --git a/include/entropy/app.h b/include/entropy/app.h index 2cf28c4..1699f96 100644 --- a/include/entropy/app.h +++ b/include/entropy/app.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -32,6 +33,13 @@ struct UiState { bool showSearchWindow = false; bool showFeatureSettings = false; bool showGeneralSettings = false; + // Update check state + std::atomic updateChecked{false}; + std::atomic updateAvailable{false}; + std::atomic updateManualRequest{false}; + std::string latestVersion; + std::string updateUrl; + std::mutex updateMutex; size_t highlighted_sector = SIZE_MAX; std::vector currentSectorData; size_t currentSectorIndex = 0; diff --git a/include/entropy/core.h b/include/entropy/core.h index 67a3d77..4b545a7 100644 --- a/include/entropy/core.h +++ b/include/entropy/core.h @@ -10,7 +10,7 @@ const size_t DEFAULT_BLOCK_WIDTH = 256; const size_t DEFAULT_BLOCK_HEIGHT = 256; const std::string ABOUT_STRING = - "EntropyVisualizer\nMatthias Hüppi, maede97@hotmail.com\nVersion " + std::string(VERSION) + " - " + std::string(DATE); + "EntropyVisualizer\nMatthias Hüppi, maede97@hotmail.com\nVersion " + std::string(EV_VERSION) + " - " + std::string(EV_DATE); const std::string HELP_STRING = R"( Entropy Visualizer - User Guide diff --git a/include/entropy/ui.h b/include/entropy/ui.h index d3a4582..6c32f4f 100644 --- a/include/entropy/ui.h +++ b/include/entropy/ui.h @@ -24,6 +24,7 @@ void renderAboutWindow(UiState &uiState); void renderHelpWindow(UiState &uiState); void renderHexViewWindow(UiState &uiState, const AppState &appState); void renderSearchWindow(UiState &uiState, AppState &appState, std::function loadHexData); +void renderUpdateWindow(UiState &uiState); void handleFileDialogs(UiState &uiState, AppState &appState, IGFD::FileDialogConfig &config, std::function loadHexData); void renderVisualization(ImDrawList *draw_list, GLuint tex, const std::vector &block_buffer, float zoom, ImVec2 pan_offset, size_t current_block, size_t block_size, size_t block_width, size_t block_height, UiState &uiState, diff --git a/include/entropy/update.h b/include/entropy/update.h new file mode 100644 index 0000000..bb4bdc1 --- /dev/null +++ b/include/entropy/update.h @@ -0,0 +1,10 @@ +#pragma once + +#include +#include + +namespace entropy { + +void startUpdateCheck(UiState &uiState, const std::string &file_path, bool manual = false); + +} // namespace entropy diff --git a/include/entropy/version.h b/include/entropy/version.h index f65ff82..758690d 100644 --- a/include/entropy/version.h +++ b/include/entropy/version.h @@ -1,4 +1,8 @@ #pragma once -#define VERSION "1.8.2" -#define DATE "10.02.2026" +namespace entropy { + +#define EV_VERSION "1.9.0" +#define EV_DATE "09.03.2026" + +} // namespace entropy \ No newline at end of file diff --git a/main.cpp b/main.cpp index 99ff903..11aac5b 100644 --- a/main.cpp +++ b/main.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include entropy::AppState *globalAppState = nullptr; @@ -48,6 +49,9 @@ int main(int argc, char **argv) { return 1; } + // Start background update check (non-blocking) + entropy::startUpdateCheck(uiState, "VERSION.txt"); + // Register ImGui settings handler for features ImGuiSettingsHandler featuresHandler; featuresHandler.TypeName = "HexDisplayFeatures"; diff --git a/src/app.cpp b/src/app.cpp index d021fab..112aaf9 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -289,7 +290,7 @@ void initializeWindowAndGL(GLFWwindow *&window, GLuint &tex) { return; const char *glsl_version = "#version 130"; - window = glfwCreateWindow(1800, 1200, (std::string("Entropy Visualizer ") + std::string(VERSION)).c_str(), nullptr, nullptr); + window = glfwCreateWindow(1800, 1200, (std::string("Entropy Visualizer ") + std::string(EV_VERSION)).c_str(), nullptr, nullptr); if (!window) return; @@ -389,7 +390,7 @@ void mainLoop(GLFWwindow *window, GLuint tex, AppState &state, UiState &uiState, if (ImGui::MenuItem("Find")) { uiState.showSearchWindow = true; } - + // Recent files submenu if (!state.recentCacheFiles.empty() && ImGui::BeginMenu("Recent Cache Files")) { for (const auto &filePair : state.recentCacheFiles) { @@ -428,6 +429,18 @@ void mainLoop(GLFWwindow *window, GLuint tex, AppState &state, UiState &uiState, } if (ImGui::BeginMenu("Misc")) { + if (ImGui::MenuItem("Check for Updates")) { + // Reset state and start an explicit check (manual) + uiState.updateChecked = false; + uiState.updateAvailable = false; + uiState.updateManualRequest = true; + { + std::lock_guard lk(uiState.updateMutex); + uiState.latestVersion.clear(); + uiState.updateUrl.clear(); + } + startUpdateCheck(uiState, "update/latest_version.txt", true); + } if (ImGui::MenuItem("About")) { uiState.showAboutUs = true; } @@ -581,6 +594,7 @@ void mainLoop(GLFWwindow *window, GLuint tex, AppState &state, UiState &uiState, renderHelpWindow(uiState); renderHexViewWindow(uiState, state); renderSearchWindow(uiState, state, loadHexData); + renderUpdateWindow(uiState); if (uiState.showFeatureSettings) { ImGui::Begin("Feature Settings", &uiState.showFeatureSettings); diff --git a/src/ui.cpp b/src/ui.cpp index 75272c3..12ae9be 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -7,6 +7,7 @@ #include #include +#include #include namespace entropy { @@ -40,6 +41,45 @@ void renderAboutWindow(UiState &uiState) { } } +void renderUpdateWindow(UiState &uiState) { + if (!uiState.updateChecked) + return; + + if (!uiState.updateAvailable && !uiState.updateManualRequest) + return; + + std::lock_guard lk(uiState.updateMutex); + ImGui::SetNextWindowSize(ImVec2(420, 140), ImGuiCond_FirstUseEver); + if (ImGui::Begin("Update", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + if (uiState.updateAvailable) { + ImGui::Text("A new version is available: %s", uiState.latestVersion.c_str()); + ImGui::Separator(); + if (ImGui::Button("Open Release Page")) { + +#ifdef __linux__ + std::string cmd = std::string("xdg-open \"") + uiState.updateUrl + "\" &"; +#elif _WIN32 + std::string cmd = std::string("start \"\" \"") + uiState.updateUrl + "\""; +#endif + std::system(cmd.c_str()); + } + ImGui::SameLine(); + if (ImGui::Button("Close")) { + uiState.updateAvailable = false; + uiState.updateManualRequest = false; + } + } else { + // Manual request and no update available + ImGui::Text("No updates available. You are running version %s", EV_VERSION); + ImGui::Separator(); + if (ImGui::Button("OK")) { + uiState.updateManualRequest = false; + } + } + } + ImGui::End(); +} + void renderHelpWindow(UiState &uiState) { if (uiState.showHelp) { ImGui::SetNextWindowSize(ImVec2(600, 600), ImGuiCond_FirstUseEver); diff --git a/src/update.cpp b/src/update.cpp new file mode 100644 index 0000000..b0a6153 --- /dev/null +++ b/src/update.cpp @@ -0,0 +1,114 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace entropy { + +static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp) { + size_t total = size * nmemb; + std::string *s = static_cast(userp); + s->append(static_cast(contents), total); + return total; +} + +static bool parse_semver(const std::string &s, int &major, int &minor, int &patch) { + std::regex re(R"((\d+)\.(\d+)\.(\d+))"); + std::smatch m; + if (std::regex_search(s, m, re)) { + major = std::stoi(m[1]); + minor = std::stoi(m[2]); + patch = std::stoi(m[3]); + return true; + } + return false; +} + +static int compare_semver(const std::string &a, const std::string &b) { + int am = 0, an = 0, ap = 0; + int bm = 0, bn = 0, bp = 0; + if (!parse_semver(a, am, an, ap) || !parse_semver(b, bm, bn, bp)) { + return 0; // unknown; treat as equal + } + if (am != bm) + return (am < bm) ? -1 : 1; + if (an != bn) + return (an < bn) ? -1 : 1; + if (ap != bp) + return (ap < bp) ? -1 : 1; + return 0; +} + +void startUpdateCheck(UiState &uiState, const std::string &file_path, bool manual) { + // Mark manual request state + if (manual) + uiState.updateManualRequest = true; + + // Run async in a detached thread + std::thread([&uiState, file_path, manual]() { + const std::string base = "https://raw.githubusercontent.com/maede97/EntropyVisualizer/refs/heads/main/"; + const std::string full_url = base + file_path; + const std::string release_page = "https://github.com/maede97/EntropyVisualizer/releases"; + + CURL *curl = curl_easy_init(); + if (!curl) { + uiState.updateChecked = true; + return; + } + + std::string response; + curl_easy_setopt(curl, CURLOPT_URL, full_url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + + CURLcode res = curl_easy_perform(curl); + if (res != CURLE_OK) { + curl_easy_cleanup(curl); + // Always mark checked; for manual requests we want the UI to show a result + uiState.updateAvailable = false; + uiState.updateChecked = true; + return; + } + + curl_easy_cleanup(curl); + + // Trim whitespace + std::string trimmed = response; + + std::cout << "Latest version string from server: '" << trimmed << "'" << std::endl; + + while (!trimmed.empty() && isspace((unsigned char)trimmed.back())) + trimmed.pop_back(); + size_t start = 0; + while (start < trimmed.size() && isspace((unsigned char)trimmed[start])) + start++; + trimmed = (start < trimmed.size()) ? trimmed.substr(start) : std::string(); + + std::string local_version = EV_VERSION; + std::string remote_version = trimmed; + + int cmp = compare_semver(local_version, remote_version); + if (cmp < 0) { + // remote is newer + { + std::lock_guard lk(uiState.updateMutex); + uiState.latestVersion = remote_version; + uiState.updateUrl = release_page; + } + uiState.updateAvailable = true; + } else { + uiState.updateAvailable = false; + } + // Mark checked so UI can react; manual requests will show the window even when not available + uiState.updateChecked = true; + }).detach(); +} + +} // namespace entropy