add volume widget
This commit is contained in:
@@ -22,6 +22,7 @@ target_sources(bar_lib
|
|||||||
src/bar/bar.cpp
|
src/bar/bar.cpp
|
||||||
src/widgets/clock.cpp
|
src/widgets/clock.cpp
|
||||||
src/widgets/workspaceIndicator.cpp
|
src/widgets/workspaceIndicator.cpp
|
||||||
|
src/widgets/volumeWidget.cpp
|
||||||
src/services/hyprland.cpp
|
src/services/hyprland.cpp
|
||||||
src/services/tray.cpp
|
src/services/tray.cpp
|
||||||
src/widgets/tray.cpp
|
src/widgets/tray.cpp
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class Bar : public Gtk::Window
|
|||||||
int m_monitorId;
|
int m_monitorId;
|
||||||
WorkspaceIndicator *m_workspaceIndicator = nullptr;
|
WorkspaceIndicator *m_workspaceIndicator = nullptr;
|
||||||
TrayWidget *m_trayWidget = nullptr;
|
TrayWidget *m_trayWidget = nullptr;
|
||||||
|
class VolumeWidget *m_volumeWidget = nullptr;
|
||||||
|
|
||||||
void setup_ui();
|
void setup_ui();
|
||||||
void load_css();
|
void load_css();
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
class SystemHelper
|
class SystemHelper
|
||||||
{
|
{
|
||||||
@@ -25,4 +28,18 @@ class SystemHelper
|
|||||||
|
|
||||||
return result;
|
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 "bar/bar.hpp"
|
||||||
|
#include "gtk/gtk.h"
|
||||||
|
#include "widgets/spacer.hpp"
|
||||||
#include "widgets/workspaceIndicator.hpp"
|
#include "widgets/workspaceIndicator.hpp"
|
||||||
|
#include "widgets/volumeWidget.hpp"
|
||||||
|
|
||||||
|
#include "helpers/systemHelper.hpp"
|
||||||
|
|
||||||
#include <gtkmm/enums.h>
|
#include <gtkmm/enums.h>
|
||||||
#include <gtkmm/label.h>
|
#include <gtkmm/label.h>
|
||||||
@@ -11,6 +16,9 @@
|
|||||||
Bar::Bar(GdkMonitor *monitor, HyprlandService &hyprlandService, TrayService &trayService, int monitorId)
|
Bar::Bar(GdkMonitor *monitor, HyprlandService &hyprlandService, TrayService &trayService, int monitorId)
|
||||||
: m_hyprlandService(hyprlandService), m_trayService(trayService), m_monitorId(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());
|
gtk_layer_init_for_window(this->gobj());
|
||||||
|
|
||||||
if (monitor)
|
if (monitor)
|
||||||
@@ -64,6 +72,11 @@ void Bar::setup_ui()
|
|||||||
clock.set_halign(Gtk::Align::CENTER);
|
clock.set_halign(Gtk::Align::CENTER);
|
||||||
clock.set_valign(Gtk::Align::CENTER);
|
clock.set_valign(Gtk::Align::CENTER);
|
||||||
center_box.append(clock);
|
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);
|
m_trayWidget = Gtk::make_managed<TrayWidget>(m_trayService);
|
||||||
right_box.append(*m_trayWidget);
|
right_box.append(*m_trayWidget);
|
||||||
@@ -73,21 +86,12 @@ void Bar::load_css()
|
|||||||
{
|
{
|
||||||
auto css_provider = Gtk::CssProvider::create();
|
auto css_provider = Gtk::CssProvider::create();
|
||||||
|
|
||||||
css_provider->load_from_data(R"(
|
// Load CSS from external resource file. Fall back to embedded CSS on error.
|
||||||
#clock-label { font-weight: bold; font-family: monospace; }
|
const std::string css = SystemHelper::read_file_to_string("resources/bar.css");
|
||||||
.workspace-pill { background-color: rgba(255, 255, 255, 0.12); border-radius: 8px; padding: 2px 8px; margin-right: 6px; }
|
css_provider->load_from_data(css);
|
||||||
.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; }
|
|
||||||
)");
|
|
||||||
|
|
||||||
Gtk::StyleContext::add_provider_for_display(
|
Gtk::StyleContext::add_provider_for_display(
|
||||||
Gdk::Display::get_default(),
|
Gdk::Display::get_default(),
|
||||||
css_provider,
|
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