From 14e22cc6363e12acbd11a11c864178adff168025 Mon Sep 17 00:00:00 2001 From: rdavidek Date: Wed, 14 Jan 2026 19:48:00 +0100 Subject: [PATCH] refactor, new features --- CMakeLists.txt | 12 ++-- build.sh | 2 +- include/config_manager.h | 6 ++ include/mainwindow.h | 3 + include/temperature_chart.h | 9 ++- src/config_manager.cpp | 17 +++-- src/mainwindow.cpp | 58 +++++++++++++++- src/temperature_chart.cpp | 130 ++++++++++++++++++++++++++++++++---- 8 files changed, 210 insertions(+), 27 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 442ba9b..5d482a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(nvme-monitor) +project(temp-monitor) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC") @@ -44,19 +44,19 @@ set(HEADERS include/config_manager.h ) -add_executable(nvme-monitor ${SOURCES} ${HEADERS}) +add_executable(temp-monitor ${SOURCES} ${HEADERS}) -target_link_libraries(nvme-monitor +target_link_libraries(temp-monitor ${GTK_LIBRARIES} ${CAIRO_LIBRARIES} m ) -target_compile_options(nvme-monitor PRIVATE ${GTK_CFLAGS_OTHER}) -target_compile_options(nvme-monitor PRIVATE ${CAIRO_CFLAGS_OTHER}) +target_compile_options(temp-monitor PRIVATE ${GTK_CFLAGS_OTHER}) +target_compile_options(temp-monitor PRIVATE ${CAIRO_CFLAGS_OTHER}) # Install executable -install(TARGETS nvme-monitor +install(TARGETS temp-monitor DESTINATION bin) # Install desktop file for system integration diff --git a/build.sh b/build.sh index 8ffee8f..945a759 100755 --- a/build.sh +++ b/build.sh @@ -8,4 +8,4 @@ cd build cmake .. make -echo "Build complete! Run with: ./nvme-monitor" +echo "Build complete! Run with: ./temp-monitor" diff --git a/include/config_manager.h b/include/config_manager.h index cf43e48..113631e 100644 --- a/include/config_manager.h +++ b/include/config_manager.h @@ -18,16 +18,20 @@ public: int getWindowWidth() const { return windowWidth; } int getWindowHeight() const { return windowHeight; } int getPollingTime() const { return pollingTime; } + int getHistoryLength() const { return historyLength; } void setWindowWidth(int w) { windowWidth = w; } void setWindowHeight(int h) { windowHeight = h; } void setPollingTime(int t) { pollingTime = t; } + void setHistoryLength(int m) { historyLength = m; } std::map getSensorColors() const { return sensorColors; } std::map getSensorNames() const { return sensorNames; } + std::map getSensorEnabled() const { return sensorEnabled; } void setSensorColor(const std::string &id, const std::string &color) { sensorColors[id] = color; } void setSensorName(const std::string &id, const std::string &name) { sensorNames[id] = name; } + void setSensorEnabled(const std::string &id, bool enabled) { sensorEnabled[id] = enabled; } private: std::string getConfigFilePath() const; @@ -36,9 +40,11 @@ private: int windowWidth; int windowHeight; int pollingTime; + int historyLength; std::map sensorColors; std::map sensorNames; + std::map sensorEnabled; }; #endif // CONFIG_MANAGER_H diff --git a/include/mainwindow.h b/include/mainwindow.h index 449b597..8cc3905 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -24,14 +24,17 @@ private: static gboolean onDeleteWindow(GtkWidget *widget, gpointer userData); static gboolean onUpdateTimer(gpointer userData); static void onRefreshRateChanged(GtkSpinButton *spinButton, gpointer userData); + static void onHistoryLengthChanged(GtkSpinButton *spinButton, gpointer userData); static void onClearButtonClicked(GtkButton *button, gpointer userData); static void onQuitButtonClicked(GtkButton *button, gpointer userData); static void onColorSet(GObject *object, GParamSpec *pspec, gpointer userData); static void onNameChanged(GtkEditable *editable, gpointer userData); + static void onVisibilityToggled(GtkCheckButton *checkButton, gpointer userData); GtkWidget *window; GtkWidget *statusLabel; GtkSpinButton *refreshRateSpinBox; + GtkSpinButton *historyLengthSpinBox; std::unique_ptr chart; std::unique_ptr monitor; std::unique_ptr config; diff --git a/include/temperature_chart.h b/include/temperature_chart.h index 47bd075..fede5c0 100644 --- a/include/temperature_chart.h +++ b/include/temperature_chart.h @@ -18,6 +18,7 @@ struct SeriesData { GdkRGBA color; std::string id; // Unique internal ID (device + sensor) std::string name; // User-friendly display name + bool visible = true; }; class TemperatureChart { @@ -40,6 +41,12 @@ public: std::string getSeriesName(const std::string &seriesId) const; void setSeriesName(const std::string &seriesId, const std::string &name); + // Visibility management + bool isSeriesVisible(const std::string &seriesId) const; + void setSeriesVisible(const std::string &seriesId, bool visible); + + void setHistoryLength(int minutes); + void drawChart(GtkDrawingArea *area, cairo_t *cr, int width, int height); private: @@ -75,7 +82,7 @@ private: // Cairo drawing state double minTemp, maxTemp; int64_t minTime, maxTime; - static constexpr int MAX_TIME_MS = 600000; // 10 minutes in milliseconds + int historyLengthMinutes; static constexpr double MIN_TEMP = 10.0; static constexpr double MAX_TEMP = 110.0; diff --git a/src/config_manager.cpp b/src/config_manager.cpp index e076709..216401b 100644 --- a/src/config_manager.cpp +++ b/src/config_manager.cpp @@ -7,7 +7,7 @@ #include ConfigManager::ConfigManager() - : windowWidth(1200), windowHeight(700), pollingTime(1) + : windowWidth(1200), windowHeight(700), pollingTime(1), historyLength(10) { } @@ -23,7 +23,7 @@ std::string ConfigManager::getConfigFilePath() const if (len == -1) { // Fallback to current directory - cachedConfigPath = "./nvme-monitor.conf"; + cachedConfigPath = "./temp-monitor.conf"; return cachedConfigPath; } @@ -34,11 +34,11 @@ std::string ConfigManager::getConfigFilePath() const size_t lastSlash = fullPath.find_last_of('/'); if (lastSlash != std::string::npos) { std::string exeDir = fullPath.substr(0, lastSlash); - cachedConfigPath = exeDir + "/nvme-monitor.conf"; + cachedConfigPath = exeDir + "/temp-monitor.conf"; return cachedConfigPath; } - cachedConfigPath = "./nvme-monitor.conf"; + cachedConfigPath = "./temp-monitor.conf"; return cachedConfigPath; } @@ -75,10 +75,14 @@ void ConfigManager::load() windowHeight = std::stoi(value); } else if (key == "polling_time") { pollingTime = std::stoi(value); + } else if (key == "history_length") { + historyLength = std::stoi(value); } else if (key.find("color_") == 0) { sensorColors[key.substr(6)] = value; } else if (key.find("name_") == 0) { sensorNames[key.substr(5)] = value; + } else if (key.find("enabled_") == 0) { + sensorEnabled[key.substr(8)] = (value == "true" || value == "1"); } } catch (const std::exception &e) { std::cerr << "Config error: invalid value for '" << key << "': " << value << " (" << e.what() << ")" << std::endl; @@ -104,6 +108,7 @@ void ConfigManager::save() file << "window_width = " << windowWidth << "\n"; file << "window_height = " << windowHeight << "\n"; file << "polling_time = " << pollingTime << "\n"; + file << "history_length = " << historyLength << "\n"; for (auto const& [id, color] : sensorColors) { file << "color_" << id << " = " << color << "\n"; @@ -112,6 +117,10 @@ void ConfigManager::save() for (auto const& [id, name] : sensorNames) { file << "name_" << id << " = " << name << "\n"; } + + for (auto const& [id, enabled] : sensorEnabled) { + file << "enabled_" << id << " = " << (enabled ? "true" : "false") << "\n"; + } file.close(); } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index cba75af..b8d9f7a 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -8,7 +8,8 @@ #include MainWindow::MainWindow() - : window(nullptr), statusLabel(nullptr), refreshRateSpinBox(nullptr), + : window(nullptr), statusLabel(nullptr), + refreshRateSpinBox(nullptr), historyLengthSpinBox(nullptr), timerID(0), refreshRateSec(3) { monitor = std::make_unique(); @@ -19,6 +20,12 @@ MainWindow::MainWindow() setupUI(); + int historyLen = config->getHistoryLength(); + chart->setHistoryLength(historyLen); + if (historyLengthSpinBox) { + gtk_spin_button_set_value(historyLengthSpinBox, historyLen); + } + // Set window size from config gtk_window_set_default_size(GTK_WINDOW(window), config->getWindowWidth(), config->getWindowHeight()); @@ -50,7 +57,7 @@ void MainWindow::setupUI() g_object_unref(provider); window = gtk_window_new(); - gtk_window_set_title(GTK_WINDOW(window), "NVMe Temperature Monitor"); + gtk_window_set_title(GTK_WINDOW(window), "Temperature Monitor"); // Set window icon - in GTK4 we load pixbuf and use it for the window display GError *error = nullptr; @@ -92,6 +99,17 @@ void MainWindow::setupUI() g_signal_connect(refreshRateSpinBox, "value-changed", G_CALLBACK(onRefreshRateChanged), this); gtk_box_append(GTK_BOX(controlBox), GTK_WIDGET(refreshRateSpinBox)); + // History length label + GtkWidget *historyLabel = gtk_label_new("History (min):"); + gtk_box_append(GTK_BOX(controlBox), historyLabel); + + // History length spinner (5-60 min, step 5) + GtkAdjustment *histAdjustment = gtk_adjustment_new(10, 5, 60, 5, 10, 0); + historyLengthSpinBox = GTK_SPIN_BUTTON(gtk_spin_button_new(histAdjustment, 5, 0)); + gtk_widget_set_size_request(GTK_WIDGET(historyLengthSpinBox), 80, -1); + g_signal_connect(historyLengthSpinBox, "value-changed", G_CALLBACK(onHistoryLengthChanged), this); + gtk_box_append(GTK_BOX(controlBox), GTK_WIDGET(historyLengthSpinBox)); + // Clear button GtkWidget *clearButton = gtk_button_new_with_label("Clear Data"); g_signal_connect(clearButton, "clicked", G_CALLBACK(onClearButtonClicked), this); @@ -161,6 +179,14 @@ void MainWindow::onRefreshRateChanged(GtkSpinButton *spinButton, gpointer userDa self->timerID = g_timeout_add(self->refreshRateSec * 1000, onUpdateTimer, self); } +void MainWindow::onHistoryLengthChanged(GtkSpinButton *spinButton, gpointer userData) +{ + MainWindow *self = static_cast(userData); + int minutes = gtk_spin_button_get_value_as_int(spinButton); + self->chart->setHistoryLength(minutes); + self->config->setHistoryLength(minutes); +} + void MainWindow::onClearButtonClicked(GtkButton *button, gpointer userData) { MainWindow *self = static_cast(userData); @@ -273,15 +299,30 @@ void MainWindow::updateLegend() auto seriesColors = chart->getSeriesColors(); // Add legend items + auto sensorEnabledMap = config->getSensorEnabled(); + for (const auto &pair : seriesColors) { const std::string &seriesId = pair.first; const GdkRGBA &color = pair.second; std::string displayName = chart->getSeriesName(seriesId); + bool isVisible = true; + if (sensorEnabledMap.find(seriesId) != sensorEnabledMap.end()) { + isVisible = sensorEnabledMap[seriesId]; + } + chart->setSeriesVisible(seriesId, isVisible); + // Create container for legend item GtkWidget *itemBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2); gtk_widget_add_css_class(itemBox, "legend-item"); + // Checkbox for visibility + GtkWidget *checkButton = gtk_check_button_new(); + gtk_check_button_set_active(GTK_CHECK_BUTTON(checkButton), isVisible); + g_object_set_data_full(G_OBJECT(checkButton), "series-name", g_strdup(seriesId.c_str()), g_free); + g_signal_connect(checkButton, "toggled", G_CALLBACK(onVisibilityToggled), this); + gtk_box_append(GTK_BOX(itemBox), checkButton); + // Create color dialog and button GtkColorDialog *dialog = gtk_color_dialog_new(); GtkWidget *colorButton = gtk_color_dialog_button_new(dialog); @@ -354,3 +395,16 @@ void MainWindow::onNameChanged(GtkEditable *editable, gpointer userData) } } } + +void MainWindow::onVisibilityToggled(GtkCheckButton *checkButton, gpointer userData) +{ + MainWindow *self = static_cast(userData); + const char *seriesId = static_cast(g_object_get_data(G_OBJECT(checkButton), "series-name")); + + if (seriesId) { + bool visible = gtk_check_button_get_active(checkButton); + self->chart->setSeriesVisible(seriesId, visible); + self->config->setSensorEnabled(seriesId, visible); + self->config->save(); + } +} diff --git a/src/temperature_chart.cpp b/src/temperature_chart.cpp index 4e678f3..398f2fd 100644 --- a/src/temperature_chart.cpp +++ b/src/temperature_chart.cpp @@ -10,7 +10,7 @@ TemperatureChart::TemperatureChart() : drawingArea(nullptr), tooltipWindow(nullptr), tooltipLabel(nullptr), maxDataPoints(600), tickHandler(0), minTemp(MIN_TEMP), maxTemp(MAX_TEMP), minTime(0), maxTime(0), - lastMouseX(-1), lastMouseY(-1) + lastMouseX(-1), lastMouseY(-1), historyLengthMinutes(10) { drawingArea = gtk_drawing_area_new(); gtk_widget_set_size_request(drawingArea, 800, 400); @@ -200,8 +200,8 @@ bool TemperatureChart::addTemperatureData(const std::string &device, const std:: seriesMap[seriesKey].points.push_back(point); - // Keep only last 10 minutes of data - int64_t cutoffTime = timestamp - MAX_TIME_MS; + // Keep only historyLengthMinutes of data + int64_t cutoffTime = timestamp - (static_cast(historyLengthMinutes) * 60 * 1000); for (auto &entry : seriesMap) { auto &points = entry.second.points; auto it = points.begin(); @@ -216,6 +216,7 @@ bool TemperatureChart::addTemperatureData(const std::string &device, const std:: bool hasData = false; for (const auto &entry : seriesMap) { + if (!entry.second.visible) continue; for (const auto &p : entry.second.points) { if (p.temperature < currentMin) currentMin = p.temperature; if (p.temperature > currentMax) currentMax = p.temperature; @@ -240,9 +241,9 @@ bool TemperatureChart::addTemperatureData(const std::string &device, const std:: maxTemp = MAX_TEMP; } - // Update time range - keep 10 minute window + // Update time range - keep dynamic window maxTime = timestamp; - minTime = timestamp - MAX_TIME_MS; + minTime = timestamp - (static_cast(historyLengthMinutes) * 60 * 1000); // Trigger redraw gtk_widget_queue_draw(drawingArea); @@ -283,6 +284,67 @@ void TemperatureChart::setSeriesColor(const std::string &seriesName, const GdkRG gtk_widget_queue_draw(drawingArea); } +void TemperatureChart::setHistoryLength(int minutes) +{ + historyLengthMinutes = minutes; + + // Immediately recalculate ranges based on new history length + if (!seriesMap.empty()) { + int64_t latestTimestamp = 0; + for (const auto &entry : seriesMap) { + if (!entry.second.points.empty()) { + latestTimestamp = std::max(latestTimestamp, entry.second.points.back().timestamp); + } + } + + if (latestTimestamp > 0) { + int64_t cutoffTime = latestTimestamp - (static_cast(historyLengthMinutes) * 60 * 1000); + + double currentMin = 1000.0; + double currentMax = -1000.0; + bool hasData = false; + + for (auto &entry : seriesMap) { + auto &points = entry.second.points; + + if (!entry.second.visible) continue; + + // Prune points that are now outside the new history window + auto it = points.begin(); + while (it != points.end() && it->timestamp < cutoffTime) { + it = points.erase(it); + } + + for (const auto &p : points) { + if (p.temperature < currentMin) currentMin = p.temperature; + if (p.temperature > currentMax) currentMax = p.temperature; + hasData = true; + } + } + + if (hasData) { + minTemp = std::floor(currentMin / 10.0) * 10.0; + maxTemp = std::ceil(currentMax / 10.0) * 10.0; + + // Ensure at least 20 degrees range + if (maxTemp - minTemp < 20.0) { + maxTemp = minTemp + 20.0; + } + + if (minTemp > 30.0) minTemp = 30.0; + } else { + minTemp = MIN_TEMP; + maxTemp = MAX_TEMP; + } + + maxTime = latestTimestamp; + minTime = cutoffTime; + } + } + + gtk_widget_queue_draw(drawingArea); +} + std::string TemperatureChart::getSeriesName(const std::string &seriesId) const { auto it = seriesMap.find(seriesId); @@ -292,6 +354,45 @@ std::string TemperatureChart::getSeriesName(const std::string &seriesId) const return seriesId; } +bool TemperatureChart::isSeriesVisible(const std::string &seriesId) const +{ + auto it = seriesMap.find(seriesId); + if (it != seriesMap.end()) { + return it->second.visible; + } + return true; +} + +void TemperatureChart::setSeriesVisible(const std::string &seriesId, bool visible) +{ + if (seriesMap.find(seriesId) != seriesMap.end()) { + seriesMap[seriesId].visible = visible; + + // Recalculate temperature range + double currentMin = 1000.0; + double currentMax = -1000.0; + bool hasVisibleData = false; + + for (const auto &entry : seriesMap) { + if (!entry.second.visible) continue; + for (const auto &p : entry.second.points) { + if (p.temperature < currentMin) currentMin = p.temperature; + if (p.temperature > currentMax) currentMax = p.temperature; + hasVisibleData = true; + } + } + + if (hasVisibleData) { + minTemp = std::floor(currentMin / 10.0) * 10.0; + maxTemp = std::ceil(currentMax / 10.0) * 10.0; + if (maxTemp - minTemp < 20.0) maxTemp = minTemp + 20.0; + if (minTemp > 30.0) minTemp = 30.0; + } + + gtk_widget_queue_draw(drawingArea); + } +} + void TemperatureChart::setSeriesName(const std::string &seriesId, const std::string &name) { if (seriesMap.find(seriesId) != seriesMap.end()) { @@ -371,13 +472,12 @@ void TemperatureChart::drawChart(GtkDrawingArea *area, cairo_t *cr, int width, i cairo_show_text(cr, tempStr); } - // Time labels on X axis (5 points for 10-minute window) + // Time labels on X axis (5 points for history window) for (int i = 0; i <= 5; i++) { double x = marginLeft + (plotWidth * i / 5.0); - // Calculate time offset in seconds - int secondsOffset = (5 - i) * 120; // 10 minutes / 5 = 2 minutes between points - int minutes = secondsOffset / 60; + // Calculate time offset in minutes + int minutes = (5 - i) * historyLengthMinutes / 5; char timeStr[16]; snprintf(timeStr, sizeof(timeStr), "-%d m", minutes); @@ -386,11 +486,11 @@ void TemperatureChart::drawChart(GtkDrawingArea *area, cairo_t *cr, int width, i cairo_show_text(cr, timeStr); } - // Draw data series (right to left, with 10-minute window) + // Draw data series (right to left, with history window) for (auto &seriesEntry : seriesMap) { SeriesData &series = seriesEntry.second; - if (series.points.empty()) continue; + if (!series.visible || series.points.empty()) continue; // Set series color cairo_set_source_rgba(cr, series.color.red, series.color.green, @@ -400,7 +500,8 @@ void TemperatureChart::drawChart(GtkDrawingArea *area, cairo_t *cr, int width, i bool firstPoint = true; for (const DataPoint &point : series.points) { // Reverse X axis - newer data on the right, older on the left - double x = marginLeft + plotWidth * (1.0 - (double)(maxTime - point.timestamp) / MAX_TIME_MS); + int64_t historyMs = static_cast(historyLengthMinutes) * 60 * 1000; + double x = marginLeft + plotWidth * (1.0 - (double)(maxTime - point.timestamp) / historyMs); double y = marginTop + plotHeight * (1.0 - (point.temperature - minTemp) / (maxTemp - minTemp)); if (firstPoint) { @@ -444,8 +545,11 @@ TemperatureChart::NearestPoint TemperatureChart::findNearestDataPoint(double mou for (auto &seriesEntry : seriesMap) { SeriesData &series = seriesEntry.second; + if (!series.visible) continue; + for (const DataPoint &point : series.points) { - double x = marginLeft + plotWidth * (1.0 - (double)(maxTime - point.timestamp) / MAX_TIME_MS); + int64_t historyMs = static_cast(historyLengthMinutes) * 60 * 1000; + double x = marginLeft + plotWidth * (1.0 - (double)(maxTime - point.timestamp) / historyMs); double y = marginTop + plotHeight * (1.0 - (point.temperature - minTemp) / (maxTemp - minTemp)); double distance = std::hypot(mouseX - x, mouseY - y);