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

@@ -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
}