From f9e8c243cfcaa793a9942adf89a5ffa781116137 Mon Sep 17 00:00:00 2001 From: Arif Hasanic Date: Mon, 5 Jan 2026 12:32:07 +0100 Subject: [PATCH] deine cousine 4. grades --- CMakeLists.txt | 1 + include/bar/bar.hpp | 4 +- include/widgets/battery.hpp | 26 +++++ resources/bar.css | 90 +++++++++++++++ src/bar/bar.cpp | 2 + src/widgets/battery.cpp | 219 ++++++++++++++++++++++++++++++++++++ 6 files changed, 341 insertions(+), 1 deletion(-) create mode 100644 include/widgets/battery.hpp create mode 100644 src/widgets/battery.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c789c54..767c431 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,7 @@ target_sources(bar_lib src/widgets/workspaceIndicator.cpp src/widgets/volumeWidget.cpp src/widgets/webWidget.cpp + src/widgets/battery.cpp src/services/todo.cpp src/services/sqliteTodoAdapter.cpp diff --git a/include/bar/bar.hpp b/include/bar/bar.hpp index 464129c..8c7fa9d 100644 --- a/include/bar/bar.hpp +++ b/include/bar/bar.hpp @@ -4,13 +4,14 @@ #include #include "icons.hpp" +#include "widgets/battery.hpp" #include "widgets/clock.hpp" +#include "widgets/controlCenter.hpp" #include "widgets/date.hpp" #include "widgets/tray.hpp" #include "widgets/volumeWidget.hpp" #include "widgets/webWidget.hpp" #include "widgets/workspaceIndicator.hpp" -#include "widgets/controlCenter.hpp" class Bar : public Gtk::Window { public: @@ -32,6 +33,7 @@ class Bar : public Gtk::Window { WorkspaceIndicator *workspaceIndicator = nullptr; TrayWidget *trayWidget = nullptr; VolumeWidget *volumeWidget = nullptr; + BatteryWidget *batteryWidget = nullptr; void setup_ui(); diff --git a/include/widgets/battery.hpp b/include/widgets/battery.hpp new file mode 100644 index 0000000..589786d --- /dev/null +++ b/include/widgets/battery.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include +#include + +class BatteryWidget : public Gtk::Box { + public: + BatteryWidget(); + ~BatteryWidget(); + + private: + Gtk::Label iconLabel; + Gtk::Label label; + sigc::connection timeoutConn; + std::filesystem::path batteryPath; + std::string currentStateClass; + + void update(); + bool on_timeout(); + void find_battery_path(); + void set_state_class(const std::string &stateClass); + std::string build_icon(int capacity, bool hasBattery, bool charging, bool full) const; + std::string build_text(int capacity, const std::string &status, bool hasBattery, bool charging, bool full) const; +}; diff --git a/resources/bar.css b/resources/bar.css index 0fc3c30..c6496a9 100644 --- a/resources/bar.css +++ b/resources/bar.css @@ -136,3 +136,93 @@ button { opacity: 1; } } + +.battery-widget { + padding: 2px 6px; + border-radius: 6px; + background-color: rgba(255, 255, 255, 0.04); +} + +.battery-widget-icon { + min-width: 7ch; + text-align: center; + font-weight: 700; + letter-spacing: 0.08ch; +} + +.battery-widget-text { + font-weight: 500; +} + +.battery-widget-normal { + background-color: rgba(120, 120, 120, 0.18); +} + +.battery-widget-full { + background-color: rgba(76, 129, 76, 0.24); +} + +.battery-widget-full .battery-widget-icon { + color: #bdf5bd; +} + +.battery-widget-charging { + background-color: rgba(76, 129, 76, 0.28); +} + +.battery-widget-charging .battery-widget-icon { + color: #9df19d; + animation: battery-charge-glow 1.4s ease-in-out infinite; +} + +.battery-widget-low { + background-color: rgba(148, 61, 61, 0.28); +} + +.battery-widget-low .battery-widget-icon { + color: #ff8585; + animation: battery-low-blink 1s steps(2, start) infinite; +} + +.battery-widget-external { + background-color: rgba(100, 100, 100, 0.18); +} + +.battery-widget-external .battery-widget-icon { + color: #d7d7d7; +} + +@keyframes battery-charge-glow { + 0% { + opacity: 0.7; + transform: translateX(0); + } + + 50% { + opacity: 1; + transform: translateX(1px); + } + + 100% { + opacity: 0.7; + transform: translateX(0); + } +} + +@keyframes battery-low-blink { + 0% { + opacity: 1; + } + + 49% { + opacity: 1; + } + + 50% { + opacity: 0.4; + } + + 100% { + opacity: 0.4; + } +} diff --git a/src/bar/bar.cpp b/src/bar/bar.cpp index c7e6849..e8a18dc 100644 --- a/src/bar/bar.cpp +++ b/src/bar/bar.cpp @@ -38,6 +38,7 @@ Bar::Bar(GdkMonitor *monitor, int monitorId) this->volumeWidget = Gtk::make_managed(); this->workspaceIndicator = Gtk::make_managed(monitorId); this->trayWidget = Gtk::make_managed(); + this->batteryWidget = Gtk::make_managed(); load_css(); setup_ui(); @@ -77,6 +78,7 @@ void Bar::setup_center_box() { void Bar::setup_right_box() { right_box.append(*this->trayWidget); + right_box.append(*this->batteryWidget); right_box.append(this->homeAssistant); right_box.append(this->controlCenter); } diff --git a/src/widgets/battery.cpp b/src/widgets/battery.cpp new file mode 100644 index 0000000..1c5d6c1 --- /dev/null +++ b/src/widgets/battery.cpp @@ -0,0 +1,219 @@ +#include "widgets/battery.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +namespace { +std::string trim(const std::string &value) { + const auto start = value.find_first_not_of(" \t\n\r"); + if (start == std::string::npos) { + return ""; + } + + const auto end = value.find_last_not_of(" \t\n\r"); + return value.substr(start, end - start + 1); +} +} + +BatteryWidget::BatteryWidget() : Gtk::Box(Gtk::Orientation::HORIZONTAL) { + set_valign(Gtk::Align::CENTER); + set_halign(Gtk::Align::CENTER); + set_spacing(6); + add_css_class("battery-widget"); + + iconLabel.set_halign(Gtk::Align::CENTER); + iconLabel.set_valign(Gtk::Align::CENTER); + iconLabel.set_width_chars(7); + iconLabel.set_text("[ ]"); + iconLabel.add_css_class("battery-widget-icon"); + + append(iconLabel); + + label.set_halign(Gtk::Align::CENTER); + label.set_valign(Gtk::Align::CENTER); + label.set_width_chars(5); + label.set_text("--%"); + label.add_css_class("battery-widget-text"); + + append(label); + + update(); + + timeoutConn = Glib::signal_timeout().connect( + sigc::mem_fun(*this, &BatteryWidget::on_timeout), 100); +} + +BatteryWidget::~BatteryWidget() { + if (timeoutConn.connected()) { + timeoutConn.disconnect(); + } +} + +void BatteryWidget::find_battery_path() { + constexpr auto base = "/sys/class/power_supply"; + + batteryPath.clear(); + + if (!std::filesystem::exists(base)) { + return; + } + + for (const auto &entry : std::filesystem::directory_iterator(base)) { + if (!entry.is_directory()) { + continue; + } + + std::ifstream typeFile(entry.path() / "type"); + if (!typeFile) { + continue; + } + + std::string type; + std::getline(typeFile, type); + type = trim(type); + std::transform(type.begin(), type.end(), type.begin(), [](unsigned char ch) { + return static_cast(std::toupper(ch)); + }); + + if (type == "BATTERY") { + batteryPath = entry.path(); + break; + } + } +} + +void BatteryWidget::update() { + try { + if (batteryPath.empty() || !std::filesystem::exists(batteryPath)) { + find_battery_path(); + } + + const bool hasBattery = !batteryPath.empty(); + + int capacity = -1; + std::string status; + if (hasBattery) { + std::ifstream capacityFile(batteryPath / "capacity"); + if (capacityFile) { + capacityFile >> capacity; + } + + std::ifstream statusFile(batteryPath / "status"); + if (statusFile) { + std::getline(statusFile, status); + } + } + + status = trim(status); + + const bool charging = status == "Charging"; + const bool full = hasBattery && (status == "Full" || (capacity >= 95)); + const bool low = (capacity >= 0 && capacity < 20 && !charging); + + std::string stateClass; + if (!hasBattery) { + stateClass = "battery-widget-external"; + } else if (charging) { + stateClass = "battery-widget-charging"; + } else if (low) { + stateClass = "battery-widget-low"; + } else if (full) { + stateClass = "battery-widget-full"; + } else { + stateClass = "battery-widget-normal"; + } + + set_state_class(stateClass); + + iconLabel.set_text(build_icon(capacity, hasBattery, charging, full)); + label.set_text(build_text(capacity, status, hasBattery, charging, full)); + } catch (...) { + set_state_class("battery-widget-normal"); + iconLabel.set_text("[???]"); + label.set_text("??%"); + } +} + +bool BatteryWidget::on_timeout() { + update(); + return true; +} + +void BatteryWidget::set_state_class(const std::string &stateClass) { + if (currentStateClass == stateClass) { + return; + } + + if (!currentStateClass.empty()) { + remove_css_class(currentStateClass); + } + + currentStateClass = stateClass; + + if (!currentStateClass.empty()) { + add_css_class(currentStateClass); + } +} + +std::string BatteryWidget::build_icon(int capacity, bool hasBattery, bool charging, bool full) const { + if (!hasBattery) { + return ""; + } + + static constexpr int segments = 5; + const int clampedCapacity = std::clamp(capacity, 0, 100); + int filledSegments = (clampedCapacity * segments + 99) / 100; + + if (full) { + filledSegments = segments; + } + + std::string icon = "["; + for (int i = 0; i < segments; ++i) { + icon += (i < filledSegments) ? '=' : ' '; + } + icon += ']'; + + if (charging) { + icon += '>'; + } else if (clampedCapacity <= 5) { + icon += '!'; + } else { + icon += ' '; + } + + return icon; +} + +std::string BatteryWidget::build_text(int capacity, const std::string &status, bool hasBattery, bool charging, bool full) const { + if (!hasBattery) { + return "AC"; + } + + if (capacity >= 0) { + std::string text; + if (full && capacity >= 99) { + text = "100%"; + } else { + text = std::to_string(std::clamp(capacity, 0, 100)) + "%"; + } + + if (charging && capacity < 100) { + text.insert(text.begin(), '+'); + } + + return text; + } + + if (!status.empty()) { + return status; + } + + return "?%"; +}