add volume widget
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
17
include/widgets/spacer.hpp
Normal file
17
include/widgets/spacer.hpp
Normal 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");
|
||||
}
|
||||
};
|
||||
24
include/widgets/volumeWidget.hpp
Normal file
24
include/widgets/volumeWidget.hpp
Normal 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
61
resources/bar.css
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
112
src/widgets/volumeWidget.cpp
Normal file
112
src/widgets/volumeWidget.cpp
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user