add volume widget

This commit is contained in:
2025-12-10 03:19:48 +01:00
parent a01bb7554b
commit 12546a36f8
8 changed files with 250 additions and 13 deletions

View File

@@ -22,6 +22,7 @@ target_sources(bar_lib
src/bar/bar.cpp
src/widgets/clock.cpp
src/widgets/workspaceIndicator.cpp
src/widgets/volumeWidget.cpp
src/services/hyprland.cpp
src/services/tray.cpp
src/widgets/tray.cpp

View File

@@ -27,6 +27,7 @@ class Bar : public Gtk::Window
int m_monitorId;
WorkspaceIndicator *m_workspaceIndicator = nullptr;
TrayWidget *m_trayWidget = nullptr;
class VolumeWidget *m_volumeWidget = nullptr;
void setup_ui();
void load_css();

View File

@@ -1,7 +1,10 @@
#pragma once
#include <array>
#include <memory>
#include <string>
#include <fstream>
#include <sstream>
class SystemHelper
{
@@ -25,4 +28,18 @@ class SystemHelper
return result;
}
// Read an entire file into a string. Throws std::runtime_error on failure.
static std::string read_file_to_string(const std::string &path)
{
std::ifstream in(path, std::ios::in | std::ios::binary);
if (!in)
{
throw std::runtime_error("Failed to open file: " + path);
}
std::ostringstream ss;
ss << in.rdbuf();
return ss.str();
}
};

View File

@@ -0,0 +1,17 @@
#pragma once
#include <gtk4-layer-shell/gtk4-layer-shell.h>
#include <gtkmm.h>
#include <gtkmm/label.h>
class Spacer : public Gtk::Label
{
public:
Spacer()
{
set_hexpand(true);
set_text("|");
set_name("spacer");
}
};

View File

@@ -0,0 +1,24 @@
#pragma once
#include <gtkmm.h>
#include <string>
#include <sigc++/sigc++.h>
class VolumeWidget : public Gtk::Box
{
public:
VolumeWidget();
virtual ~VolumeWidget();
// Refresh displayed volume from the system
void update();
protected:
// timeout handler for periodic polling; return true to keep polling
bool on_timeout();
private:
Gtk::Label m_label;
Glib::RefPtr<Gtk::GestureClick> m_click;
sigc::connection m_timeoutConn;
};

61
resources/bar.css Normal file
View File

@@ -0,0 +1,61 @@
* {
all: unset; /* Tries to remove all styling */
}
window {
/* sleak modern */
background-color: rgba(30, 30, 30, 0.8);
color: #ffffff;
font-family: "IBMPlexSans-Regular", sans-serif;
font-size: 14px;
padding: 2px 7px;
}
#clock-label {
font-weight: bold;
font-family: monospace;
}
.workspace-pill {
background-color: rgba(255, 255, 255, 0.12);
padding: 2px 5px;
margin-right: 6px;
border-radius: 5px;
}
.workspace-pill:last-child {
margin-right: 0;
}
.workspace-pill-focused {
background-color: rgba(255, 255, 255, 0.18);
}
.workspace-pill-active {
background-color: rgba(255, 255, 255, 0.25);
}
.workspace-pill-urgent {
background-color: #ff5555;
color: #111;
}
/* Hover effect: slightly brighten background and show pointer */
.workspace-pill:hover {
background-color: rgba(255, 255, 255, 0.20);
}
.tray-icon {
padding: 0;
margin: 0;
border: none;
background: transparent;
min-width: 0;
min-height: 0;
}
#spacer {
color: rgba(255, 255, 255, 0.3);
padding: 0 5px;
}

View File

@@ -1,5 +1,10 @@
#include "bar/bar.hpp"
#include "gtk/gtk.h"
#include "widgets/spacer.hpp"
#include "widgets/workspaceIndicator.hpp"
#include "widgets/volumeWidget.hpp"
#include "helpers/systemHelper.hpp"
#include <gtkmm/enums.h>
#include <gtkmm/label.h>
@@ -11,6 +16,9 @@
Bar::Bar(GdkMonitor *monitor, HyprlandService &hyprlandService, TrayService &trayService, int monitorId)
: m_hyprlandService(hyprlandService), m_trayService(trayService), m_monitorId(monitorId)
{
// Name the window so CSS can be scoped specifically to this bar.
set_name("bar-window");
gtk_layer_init_for_window(this->gobj());
if (monitor)
@@ -64,6 +72,11 @@ void Bar::setup_ui()
clock.set_halign(Gtk::Align::CENTER);
clock.set_valign(Gtk::Align::CENTER);
center_box.append(clock);
center_box.append(*(new Spacer()));
// Volume widget placed after spacer
m_volumeWidget = Gtk::make_managed<VolumeWidget>();
center_box.append(*m_volumeWidget);
m_trayWidget = Gtk::make_managed<TrayWidget>(m_trayService);
right_box.append(*m_trayWidget);
@@ -73,21 +86,12 @@ void Bar::load_css()
{
auto css_provider = Gtk::CssProvider::create();
css_provider->load_from_data(R"(
#clock-label { font-weight: bold; font-family: monospace; }
.workspace-pill { background-color: rgba(255, 255, 255, 0.12); border-radius: 8px; padding: 2px 8px; margin-right: 6px; }
.workspace-pill:last-child { margin-right: 0; }
.workspace-pill-focused { background-color: rgba(255, 255, 255, 0.18); }
.workspace-pill-active { background-color: rgba(255, 255, 255, 0.25); }
.workspace-pill-urgent { background-color: #ff5555; color: #111; }
/* Hover effect: slightly brighten background and show pointer */
.workspace-pill:hover { background-color: rgba(255, 255, 255, 0.20); }
.workspace-pill:hover { cursor: pointer; } // TODO: cursors has to be set differently in GTK4?
.tray-icon { padding: 0; margin: 0; border: none; background: transparent; min-width: 0; min-height: 0; }
)");
// Load CSS from external resource file. Fall back to embedded CSS on error.
const std::string css = SystemHelper::read_file_to_string("resources/bar.css");
css_provider->load_from_data(css);
Gtk::StyleContext::add_provider_for_display(
Gdk::Display::get_default(),
css_provider,
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
GTK_STYLE_PROVIDER_PRIORITY_USER + 1);
}

View File

@@ -0,0 +1,112 @@
#include "widgets/volumeWidget.hpp"
#include "helpers/systemHelper.hpp"
#include <sigc++/functors/mem_fun.h>
#include <regex>
#include <iostream>
#include <cmath>
VolumeWidget::VolumeWidget()
: Gtk::Box(Gtk::Orientation::HORIZONTAL)
{
set_valign(Gtk::Align::CENTER);
set_halign(Gtk::Align::CENTER);
m_label.set_halign(Gtk::Align::CENTER);
m_label.set_valign(Gtk::Align::CENTER);
m_label.set_text("Vol");
append(m_label);
// Click toggles mute using wpctl
m_click = Gtk::GestureClick::create();
m_click->set_button(GDK_BUTTON_PRIMARY);
// signal_released provides (int, double, double) — use lambda to ignore args
m_click->signal_released().connect([this](int /*n_press*/, double /*x*/, double /*y*/)
{
try
{
// Toggle mute then refresh
(void)SystemHelper::get_command_output("wpctl set-mute @DEFAULT_SINK@ toggle");
}
catch (const std::exception &ex)
{
std::cerr << "[VolumeWidget] failed to toggle mute: " << ex.what() << std::endl;
}
this->update();
});
add_controller(m_click);
// Initial read
update();
// Start polling every 1 second to keep the display up to date
m_timeoutConn = Glib::signal_timeout().connect(sigc::mem_fun(*this, &VolumeWidget::on_timeout), 100);
}
VolumeWidget::~VolumeWidget()
{
if (m_timeoutConn.connected())
m_timeoutConn.disconnect();
}
void VolumeWidget::update()
{
try
{
const std::string out = SystemHelper::get_command_output("wpctl get-volume @DEFAULT_SINK@");
// Attempt to parse a number (percentage or fraction)
std::smatch m;
std::regex r_percent(R"((\d+(?:\.\d+)?)%)");
std::regex r_number(R"((\d+(?:\.\d+)?))");
std::string text = out;
int percent = -1;
if (std::regex_search(text, m, r_percent))
{
percent = static_cast<int>(std::round(std::stod(m[1].str())));
}
else if (std::regex_search(text, m, r_number))
{
// If number looks like 0.8 treat as fraction
const double v = std::stod(m[1].str());
if (v <= 1.0)
percent = static_cast<int>(std::round(v * 100.0));
else
percent = static_cast<int>(std::round(v));
}
if (percent >= 0)
{
m_label.set_text(std::to_string(percent) + "%");
}
else
{
// Fallback to raw output (trimmed)
auto pos = text.find_first_not_of(" \t\n\r");
if (pos != std::string::npos)
{
auto end = text.find_last_not_of(" \t\n\r");
m_label.set_text(text.substr(pos, end - pos + 1));
}
else
{
m_label.set_text("?");
}
}
}
catch (const std::exception &ex)
{
std::cerr << "[VolumeWidget] failed to read volume: " << ex.what() << std::endl;
m_label.set_text("N/A");
}
}
bool VolumeWidget::on_timeout()
{
update();
return true; // keep timeout active
}