Merge branch 'master' into feat/bt-multiple-batteries

This commit is contained in:
Alexis Rouillard
2026-07-03 22:22:38 +02:00
committed by GitHub
75 changed files with 1102 additions and 386 deletions
+9
View File
@@ -4,6 +4,8 @@
#include <gtkmm/label.h> #include <gtkmm/label.h>
#include <json/json.h> #include <json/json.h>
#include <optional>
#include "AModule.hpp" #include "AModule.hpp"
namespace waybar { namespace waybar {
@@ -25,12 +27,19 @@ class ALabel : public AModule {
bool alt_ = false; bool alt_ = false;
std::string default_format_; std::string default_format_;
bool setLabelMarkup(const Glib::ustring& markup);
bool setTooltipMarkup(const Glib::ustring& markup);
bool handleToggle(GdkEventButton* const& e) override; bool handleToggle(GdkEventButton* const& e) override;
virtual std::string getState(uint8_t value, bool lesser = false); virtual std::string getState(uint8_t value, bool lesser = false);
std::map<std::string, GtkMenuItem*> submenus_; std::map<std::string, GtkMenuItem*> submenus_;
std::map<std::string, std::string> menuActionsMap_; std::map<std::string, std::string> menuActionsMap_;
static void handleGtkMenuEvent(GtkMenuItem* menuitem, gpointer data); static void handleGtkMenuEvent(GtkMenuItem* menuitem, gpointer data);
private:
std::optional<Glib::ustring> last_label_markup_;
std::optional<Glib::ustring> last_tooltip_markup_;
}; };
} // namespace waybar } // namespace waybar
+7
View File
@@ -25,6 +25,10 @@ class AModule : public IModule {
bool expandEnabled() const; bool expandEnabled() const;
virtual void suspend() {};
virtual void resume() {};
bool shouldSuspend() const { return disable_on_sleep_; }
protected: protected:
// Don't need to make an object directly // Don't need to make an object directly
// Derived classes are able to use it // Derived classes are able to use it
@@ -48,6 +52,8 @@ class AModule : public IModule {
virtual bool handleMouseLeave(GdkEventCrossing* const& ev); virtual bool handleMouseLeave(GdkEventCrossing* const& ev);
virtual bool handleScroll(GdkEventScroll*); virtual bool handleScroll(GdkEventScroll*);
virtual bool handleRelease(GdkEventButton* const& ev); virtual bool handleRelease(GdkEventButton* const& ev);
bool disable_on_sleep_{false};
GObject* menu_ = nullptr; GObject* menu_ = nullptr;
private: private:
@@ -57,6 +63,7 @@ class AModule : public IModule {
bool hasUserEvents_; bool hasUserEvents_;
gdouble distance_scrolled_y_; gdouble distance_scrolled_y_;
gdouble distance_scrolled_x_; gdouble distance_scrolled_x_;
sigc::connection cursor_timeout_conn_;
std::map<std::string, std::string> eventActionMap_; std::map<std::string, std::string> eventActionMap_;
static const inline std::map<std::pair<uint, GdkEventType>, std::string> eventMap_{ static const inline std::map<std::pair<uint, GdkEventType>, std::string> eventMap_{
{std::make_pair(1, GdkEventType::GDK_BUTTON_PRESS), "on-click"}, {std::make_pair(1, GdkEventType::GDK_BUTTON_PRESS), "on-click"},
+3
View File
@@ -75,6 +75,8 @@ class Bar : public sigc::trackable {
util::KillSignalAction getOnSigusr1Action(); util::KillSignalAction getOnSigusr1Action();
util::KillSignalAction getOnSigusr2Action(); util::KillSignalAction getOnSigusr2Action();
void toggleSuspend(bool suspend);
struct waybar_output* output; struct waybar_output* output;
Json::Value config; Json::Value config;
struct wl_surface* surface; struct wl_surface* surface;
@@ -99,6 +101,7 @@ class Bar : public sigc::trackable {
void setMode(const bar_mode&); void setMode(const bar_mode&);
void setPassThrough(bool passthrough); void setPassThrough(bool passthrough);
void setPosition(Gtk::PositionType position); void setPosition(Gtk::PositionType position);
void forceLayerCommit();
void onConfigure(GdkEventConfigure* ev); void onConfigure(GdkEventConfigure* ev);
void configureGlobalOffset(int width, int height); void configureGlobalOffset(int width, int height);
void onOutputGeometryChanged(); void onOutputGeometryChanged();
+3
View File
@@ -44,6 +44,7 @@ class Client {
const char* interface, uint32_t version); const char* interface, uint32_t version);
static void handleGlobalRemove(void* data, struct wl_registry* registry, uint32_t name); static void handleGlobalRemove(void* data, struct wl_registry* registry, uint32_t name);
static void handleOutputDone(void*, struct zxdg_output_v1*); static void handleOutputDone(void*, struct zxdg_output_v1*);
void createBarsBatch();
static void handleOutputName(void*, struct zxdg_output_v1*, const char*); static void handleOutputName(void*, struct zxdg_output_v1*, const char*);
static void handleOutputDescription(void*, struct zxdg_output_v1*, const char*); static void handleOutputDescription(void*, struct zxdg_output_v1*, const char*);
void handleMonitorAdded(Glib::RefPtr<Gdk::Monitor> monitor); void handleMonitorAdded(Glib::RefPtr<Gdk::Monitor> monitor);
@@ -58,6 +59,8 @@ class Client {
std::string m_cssFile; std::string m_cssFile;
sigc::connection monitor_added_connection_; sigc::connection monitor_added_connection_;
sigc::connection monitor_removed_connection_; sigc::connection monitor_removed_connection_;
std::list<waybar_output*> pending_outputs_;
bool bars_scheduled_ = false;
}; };
} // namespace waybar } // namespace waybar
+12
View File
@@ -30,6 +30,7 @@ class Workspace {
public: public:
explicit Workspace(const Json::Value& workspace_data, Workspaces& workspace_manager, explicit Workspace(const Json::Value& workspace_data, Workspaces& workspace_manager,
const Json::Value& clients_data = Json::Value::nullRef); const Json::Value& clients_data = Json::Value::nullRef);
~Workspace();
std::string& selectIcon(std::map<std::string, std::string>& icons_map); std::string& selectIcon(std::map<std::string, std::string>& icons_map);
Gtk::Button& button() { return m_button; }; Gtk::Button& button() { return m_button; };
@@ -45,6 +46,15 @@ class Workspace {
bool isUrgent() const { return m_isUrgent; }; bool isUrgent() const { return m_isUrgent; };
bool handleClicked(GdkEventButton* bt) const; bool handleClicked(GdkEventButton* bt) const;
bool handleEnter(GdkEventCrossing* event);
bool handleLeave(GdkEventCrossing* event);
void startHoverCheck();
void stopHoverCheck();
bool syncHoverClass();
bool pointerInsideButton();
void setActive(bool value = true) { m_isActive = value; }; void setActive(bool value = true) { m_isActive = value; };
void setPersistentRule(bool value = true) { m_isPersistentRule = value; }; void setPersistentRule(bool value = true) { m_isPersistentRule = value; };
void setPersistentConfig(bool value = true) { m_isPersistentConfig = value; }; void setPersistentConfig(bool value = true) { m_isPersistentConfig = value; };
@@ -80,6 +90,8 @@ class Workspace {
bool m_isUrgent = false; bool m_isUrgent = false;
bool m_isVisible = false; bool m_isVisible = false;
sigc::connection m_hoverCheckConnection;
std::vector<WindowRepr> m_windowMap; std::vector<WindowRepr> m_windowMap;
Gtk::Button m_button; Gtk::Button m_button;
-2
View File
@@ -19,8 +19,6 @@ class Memory : public ALabel {
private: private:
void parseMeminfo(); void parseMeminfo();
static float calc_divisor(const std::string& divisor);
std::unordered_map<std::string, unsigned long> meminfo_; std::unordered_map<std::string, unsigned long> meminfo_;
util::SleeperThread thread_; util::SleeperThread thread_;
+5 -1
View File
@@ -33,6 +33,7 @@ class MPD : public ALabel {
detail::unique_status status_; detail::unique_status status_;
mpd_state state_; mpd_state state_;
detail::unique_song song_; detail::unique_song song_;
std::string ellipsis_;
public: public:
MPD(const std::string&, const Json::Value&); MPD(const std::string&, const Json::Value&);
@@ -45,6 +46,10 @@ class MPD : public ALabel {
void setLabel(); void setLabel();
std::string getStateIcon() const; std::string getStateIcon() const;
std::string getOptionIcon(const std::string& optionName, bool activated) const; std::string getOptionIcon(const std::string& optionName, bool activated) const;
std::string getArtistStr(bool truncated) const;
std::string getAlbumArtistStr(bool truncated) const;
std::string getAlbumStr(bool truncated) const;
std::string getTitleStr(bool truncated) const;
// GUI-side methods // GUI-side methods
bool handlePlayPause(GdkEventButton* const&); bool handlePlayPause(GdkEventButton* const&);
@@ -54,7 +59,6 @@ class MPD : public ALabel {
void tryConnect(); void tryConnect();
void checkErrors(mpd_connection* conn); void checkErrors(mpd_connection* conn);
void fetchState(); void fetchState();
void queryMPD();
inline bool stopped() const { return connection_ && state_ == MPD_STATE_STOP; } inline bool stopped() const { return connection_ && state_ == MPD_STATE_STOP; }
inline bool playing() const { return connection_ && state_ == MPD_STATE_PLAY; } inline bool playing() const { return connection_ && state_ == MPD_STATE_PLAY; }
-1
View File
@@ -197,7 +197,6 @@ class Context {
void tryConnect() const; void tryConnect() const;
void checkErrors(mpd_connection*) const; void checkErrors(mpd_connection*) const;
void do_update(); void do_update();
void queryMPD() const;
void fetchState() const; void fetchState() const;
constexpr mpd_state state() const; constexpr mpd_state state() const;
void emit() const; void emit() const;
+2 -2
View File
@@ -1,13 +1,14 @@
#pragma once #pragma once
namespace detail { namespace detail {
using namespace std::literals::chrono_literals;
inline bool Context::is_connected() const { return mpd_module_->connection_ != nullptr; } inline bool Context::is_connected() const { return mpd_module_->connection_ != nullptr; }
inline bool Context::is_playing() const { return mpd_module_->playing(); } inline bool Context::is_playing() const { return mpd_module_->playing(); }
inline bool Context::is_paused() const { return mpd_module_->paused(); } inline bool Context::is_paused() const { return mpd_module_->paused(); }
inline bool Context::is_stopped() const { return mpd_module_->stopped(); } inline bool Context::is_stopped() const { return mpd_module_->stopped(); }
constexpr inline std::size_t Context::interval() const { return mpd_module_->interval_.count(); } constexpr inline std::size_t Context::interval() const { return mpd_module_->interval_ / 1s; }
inline void Context::tryConnect() const { mpd_module_->tryConnect(); } inline void Context::tryConnect() const { mpd_module_->tryConnect(); }
inline unique_connection& Context::connection() { return mpd_module_->connection_; } inline unique_connection& Context::connection() { return mpd_module_->connection_; }
constexpr inline mpd_state Context::state() const { return mpd_module_->state_; } constexpr inline mpd_state Context::state() const { return mpd_module_->state_; }
@@ -15,7 +16,6 @@ constexpr inline mpd_state Context::state() const { return mpd_module_->state_;
inline void Context::do_update() { mpd_module_->setLabel(); } inline void Context::do_update() { mpd_module_->setLabel(); }
inline void Context::checkErrors(mpd_connection* conn) const { mpd_module_->checkErrors(conn); } inline void Context::checkErrors(mpd_connection* conn) const { mpd_module_->checkErrors(conn); }
inline void Context::queryMPD() const { mpd_module_->queryMPD(); }
inline void Context::fetchState() const { mpd_module_->fetchState(); } inline void Context::fetchState() const { mpd_module_->fetchState(); }
inline void Context::emit() const { mpd_module_->emit(); } inline void Context::emit() const { mpd_module_->emit(); }
+1
View File
@@ -28,6 +28,7 @@ class Privacy : public AModule {
// Config // Config
Gtk::Box box_; Gtk::Box box_;
std::vector<PrivacyItem*> modules_;
uint iconSpacing = 4; uint iconSpacing = 4;
uint iconSize = 20; uint iconSize = 20;
uint transition_duration = 250; uint transition_duration = 250;
+10 -7
View File
@@ -6,11 +6,6 @@
#include "util/audio_backend.hpp" #include "util/audio_backend.hpp"
namespace waybar::modules { namespace waybar::modules {
enum class PulseaudioSliderTarget {
Sink,
Source,
};
class PulseaudioSlider : public ASlider { class PulseaudioSlider : public ASlider {
public: public:
PulseaudioSlider(const std::string&, const Json::Value&); PulseaudioSlider(const std::string&, const Json::Value&);
@@ -21,7 +16,15 @@ class PulseaudioSlider : public ASlider {
private: private:
std::shared_ptr<util::AudioBackend> backend = nullptr; std::shared_ptr<util::AudioBackend> backend = nullptr;
PulseaudioSliderTarget target = PulseaudioSliderTarget::Sink; util::PulseaudioTarget target = util::PulseaudioTarget::Sink;
bool zero_on_mute = true;
bool unmute_on_volume_change = true;
// zero_on_mute and unmute_on_volume_change default to true
// in order to maintain previous behaviour when using a
// config in which these values are undefined
bool previously_muted = false;
}; };
} // namespace waybar::modules } // namespace waybar::modules
+2
View File
@@ -43,6 +43,8 @@ class Host {
std::size_t watcher_id_; std::size_t watcher_id_;
GCancellable* cancellable_ = nullptr; GCancellable* cancellable_ = nullptr;
SnWatcher* watcher_ = nullptr; SnWatcher* watcher_ = nullptr;
sigc::connection retry_connection_;
unsigned retry_count_ = 0;
const Json::Value& config_; const Json::Value& config_;
const Bar& bar_; const Bar& bar_;
const std::function<void(std::unique_ptr<Item>&)> on_add_; const std::function<void(std::unique_ptr<Item>&)> on_add_;
+1
View File
@@ -46,6 +46,7 @@ class Item : public sigc::trackable {
std::string title; std::string title;
std::string icon_name; std::string icon_name;
Glib::RefPtr<Gdk::Pixbuf> icon_pixmap; Glib::RefPtr<Gdk::Pixbuf> icon_pixmap;
bool has_custom_icon_ = false;
Glib::RefPtr<Gtk::IconTheme> icon_theme; Glib::RefPtr<Gtk::IconTheme> icon_theme;
std::string overlay_icon_name; std::string overlay_icon_name;
Glib::RefPtr<Gdk::Pixbuf> overlay_icon_pixmap; Glib::RefPtr<Gdk::Pixbuf> overlay_icon_pixmap;
-1
View File
@@ -22,7 +22,6 @@ class Tray : public AModule {
void queueUpdate(); void queueUpdate();
static inline std::size_t nb_hosts_ = 0; static inline std::size_t nb_hosts_ = 0;
bool show_passive_ = false;
Gtk::Box box_; Gtk::Box box_;
SNI::Watcher::singleton watcher_; SNI::Watcher::singleton watcher_;
SNI::Host host_; SNI::Host host_;
+1
View File
@@ -24,6 +24,7 @@ class Workspaces : public AModule, public sigc::trackable {
private: private:
static constexpr std::string_view workspace_switch_cmd_ = "workspace {} \"{}\""; static constexpr std::string_view workspace_switch_cmd_ = "workspace {} \"{}\"";
static constexpr std::string_view workspace_switch_number_cmd_ = "workspace {} number {}";
static constexpr std::string_view persistent_workspace_switch_cmd_ = static constexpr std::string_view persistent_workspace_switch_cmd_ =
R"(workspace {} "{}"; move workspace to output "{}"; workspace {} "{}")"; R"(workspace {} "{}"; move workspace to output "{}"; workspace {} "{}")";
+2
View File
@@ -14,6 +14,8 @@ class Temperature : public ALabel {
Temperature(const std::string&, const Json::Value&); Temperature(const std::string&, const Json::Value&);
virtual ~Temperature() = default; virtual ~Temperature() = default;
auto update() -> void override; auto update() -> void override;
void suspend() override;
void resume() override;
private: private:
float getTemperature(); float getTemperature();
-5
View File
@@ -12,11 +12,6 @@
#include <type_traits> #include <type_traits>
#include <utility> #include <utility>
#ifdef __OpenBSD__
#define SIGRTMIN SIGUSR1 - 1
#define SIGRTMAX SIGUSR1 + 1
#endif
namespace waybar { namespace waybar {
/** /**
+10 -1
View File
@@ -14,6 +14,11 @@
namespace waybar::util { namespace waybar::util {
enum class PulseaudioTarget {
Sink,
Source,
};
class AudioBackend { class AudioBackend {
private: private:
static void subscribeCb(pa_context*, pa_subscription_event_type_t, uint32_t, void*); static void subscribeCb(pa_context*, pa_subscription_event_type_t, uint32_t, void*);
@@ -92,7 +97,11 @@ class AudioBackend {
void toggleSourceMute(); void toggleSourceMute();
void toggleSourceMute(bool); void toggleSourceMute(bool);
uint16_t getVolume(PulseaudioTarget) const;
bool getMuted(PulseaudioTarget) const;
void unmute(PulseaudioTarget);
bool isBluetooth(); bool isBluetooth();
}; };
} // namespace waybar::util } // namespace waybar::util
+4 -2
View File
@@ -14,12 +14,14 @@ struct pollfd;
namespace waybar { namespace waybar {
class CssReloadHelper { class CssReloadHelper {
public: public:
CssReloadHelper(std::string cssFile, std::function<void()> callback); CssReloadHelper(std::string cssFile, std::function<void(const std::string&)> callback);
virtual ~CssReloadHelper() = default; virtual ~CssReloadHelper() = default;
virtual void monitorChanges(); virtual void monitorChanges();
virtual void changeCssFile(const std::string& newCssFile);
protected: protected:
std::vector<std::string> parseImports(const std::string& cssFile); std::vector<std::string> parseImports(const std::string& cssFile);
@@ -42,7 +44,7 @@ class CssReloadHelper {
private: private:
std::string m_cssFile; std::string m_cssFile;
std::function<void()> m_callback; std::function<void(const std::string&)> m_callback;
std::vector<std::tuple<Glib::RefPtr<Gio::FileMonitor>>> m_fileMonitors; std::vector<std::tuple<Glib::RefPtr<Gio::FileMonitor>>> m_fileMonitors;
}; };
+9 -3
View File
@@ -7,6 +7,7 @@
#include <codecvt> #include <codecvt>
#include <iostream> #include <iostream>
#include <locale> #include <locale>
#include <memory>
#include <regex> #include <regex>
#if (FMT_VERSION >= 90000) #if (FMT_VERSION >= 90000)
@@ -26,14 +27,19 @@ class JsonParser {
Json::Value root; Json::Value root;
// replace all occurrences of "\x" with "\u00", because JSON doesn't allow "\x" escape sequences // replace all occurrences of "\x" with "\u00", because JSON doesn't allow "\x" escape sequences
std::string modifiedJsonStr = replaceHexadecimalEscape(jsonStr); std::string modifiedJsonStr;
const std::string* json = &jsonStr;
if (jsonStr.find("\\x") != std::string::npos) {
modifiedJsonStr = replaceHexadecimalEscape(jsonStr);
json = &modifiedJsonStr;
}
std::istringstream jsonStream(modifiedJsonStr);
std::string errs; std::string errs;
// Use local CharReaderBuilder for thread safety - the IPC singleton's // Use local CharReaderBuilder for thread safety - the IPC singleton's
// parser can be called concurrently from multiple module threads // parser can be called concurrently from multiple module threads
Json::CharReaderBuilder readerBuilder; Json::CharReaderBuilder readerBuilder;
if (!Json::parseFromStream(readerBuilder, jsonStream, &root, &errs)) { auto reader = std::unique_ptr<Json::CharReader>(readerBuilder.newCharReader());
if (!reader->parse(json->data(), json->data() + json->size(), &root, &errs)) {
throw std::runtime_error("Error parsing JSON: " + errs); throw std::runtime_error("Error parsing JSON: " + errs);
} }
return root; return root;
+24
View File
@@ -79,6 +79,12 @@ class SleeperThread {
auto sleep_for(std::chrono::system_clock::duration dur) { auto sleep_for(std::chrono::system_clock::duration dur) {
std::unique_lock lk(mutex_); std::unique_lock lk(mutex_);
CancellationGuard cancel_lock; CancellationGuard cancel_lock;
condvar_.wait(lk, [this] {
return !is_paused_ || signal_.load(std::memory_order_relaxed) ||
!do_run_.load(std::memory_order_relaxed);
});
constexpr auto max_time_point = std::chrono::steady_clock::time_point::max(); constexpr auto max_time_point = std::chrono::steady_clock::time_point::max();
auto wait_end = max_time_point; auto wait_end = max_time_point;
auto now = std::chrono::steady_clock::now(); auto now = std::chrono::steady_clock::now();
@@ -95,6 +101,12 @@ class SleeperThread {
time_point) { time_point) {
std::unique_lock lk(mutex_); std::unique_lock lk(mutex_);
CancellationGuard cancel_lock; CancellationGuard cancel_lock;
condvar_.wait(lk, [this] {
return !is_paused_ || signal_.load(std::memory_order_relaxed) ||
!do_run_.load(std::memory_order_relaxed);
});
return condvar_.wait_until(lk, time_point, [this] { return condvar_.wait_until(lk, time_point, [this] {
return signal_.load(std::memory_order_relaxed) || !do_run_.load(std::memory_order_relaxed); return signal_.load(std::memory_order_relaxed) || !do_run_.load(std::memory_order_relaxed);
}); });
@@ -122,6 +134,17 @@ class SleeperThread {
} }
} }
void pause() {
std::lock_guard<std::mutex> lock(mutex_);
is_paused_ = true;
}
void resume() {
std::lock_guard<std::mutex> lock(mutex_);
is_paused_ = false;
condvar_.notify_all();
}
~SleeperThread() { ~SleeperThread() {
connection_.disconnect(); connection_.disconnect();
stop(); stop();
@@ -137,6 +160,7 @@ class SleeperThread {
std::atomic<bool> do_run_ = true; std::atomic<bool> do_run_ = true;
std::atomic<bool> signal_ = false; std::atomic<bool> signal_ = false;
sigc::connection connection_; sigc::connection connection_;
bool is_paused_{false};
}; };
} // namespace waybar::util } // namespace waybar::util
+7
View File
@@ -0,0 +1,7 @@
#pragma once
#include <string>
namespace waybar::util {
size_t utf8_width(const std::string& str);
void utf8_truncate(std::string& s, const std::string& ellipsis, size_t max_len);
} // namespace waybar::util
+5
View File
@@ -27,6 +27,11 @@ The *battery* module displays the current capacity and state (eg. charging) of y
default: false ++ default: false ++
Option to use the battery design capacity instead of its current maximal capacity. Option to use the battery design capacity instead of its current maximal capacity.
*full-at-plugged*: ++
typeof: bool ++
default: false ++
When enabled, a battery that is *Full* while the adapter is online is reported with the *Plugged* status instead of *Full* (so you can style/format it separately). Disabled by default to preserve the existing *Full* behaviour.
*interval*: ++ *interval*: ++
typeof: integer ++ typeof: integer ++
default: 60 ++ default: 60 ++
+6
View File
@@ -92,6 +92,12 @@ The *cpu* module displays the current CPU utilization.
*{load}*: Current CPU load. *{load}*: Current CPU load.
*{load1}*: CPU load average over the last minute.
*{load5}*: CPU load average over the last 5 minutes.
*{load15}*: CPU load average over the last 15 minutes.
*{usage}*: Current overall CPU usage. *{usage}*: Current overall CPU usage.
*{usage*{n}*}*: Current CPU core n usage. Cores are numbered from zero, so first core will be {usage0} and 4th will be {usage3}. *{usage*{n}*}*: Current CPU core n usage. Cores are numbered from zero, so first core will be {usage0} and 4th will be {usage3}.
+36
View File
@@ -38,6 +38,20 @@ Addressed by *hyprland/language*
typeof: array ++ typeof: array ++
The actions corresponding to the buttons of the menu. The actions corresponding to the buttons of the menu.
*tooltip*: ++
typeof: boolean ++
default: true ++
Enables or disables the tooltip for the language module. By default, the tooltip is enabled. Set to *false* to disable.
*tooltip-format*: ++
typeof: string ++
default: {long} ++
Specifies the format of the tooltip when it is enabled. It follows the same format replacement rules as the *format* key.
*tooltip-format-<lang>*: ++
typeof: string ++
Allows specifying a different tooltip format for each language. The *<lang>* should be replaced with the language code. This can be used to provide a custom tooltip for each language.
*expand*: ++ *expand*: ++
typeof: bool ++ typeof: bool ++
default: false ++ default: false ++
@@ -66,6 +80,28 @@ Addressed by *hyprland/language*
} }
``` ```
```
"hyprland/language": {
"format": "{}",
"format-en": "US",
"format-es": "ES",
"tooltip": true,
"tooltip-format": "{long}"
}
```
```
"hyprland/language": {
"format": "{}",
"format-en": "US",
"format-es": "ES",
"tooltip": true,
"tooltip-format": "{}",
"tooltip-format-es": "{Español}",
"tooltip-format-en": "{English (american)}"
}
```
# STYLE # STYLE
- *#language* - *#language*
+8
View File
@@ -135,6 +135,14 @@ This setting is ignored if *workspace-taskbar.enable* is set to true.
default: false ++ default: false ++
If set to false, you can't scroll to cycle throughout workspaces from the entire bar. If set to true this behaviour is enabled. If set to false, you can't scroll to cycle throughout workspaces from the entire bar. If set to true this behaviour is enabled.
*on-scroll-up*: ++
typeof: string ++
Command to execute when scrolling up on the module. This replaces the default behaviour of workspace cycling.
*on-scroll-down*: ++
typeof: string ++
Command to execute when scrolling down on the module. This replaces the default behaviour of workspace cycling.
*ignore-workspaces*: ++ *ignore-workspaces*: ++
typeof: array ++ typeof: array ++
default: [] ++ default: [] ++
+24 -4
View File
@@ -28,9 +28,20 @@ The volume can be controlled by dragging the slider across the bar or clicking o
The orientation of the slider. Can be either `horizontal` or `vertical`. The orientation of the slider. Can be either `horizontal` or `vertical`.
*expand*: ++ *expand*: ++
typeof: bool ++ typeof: bool ++
default: false ++ default: false ++
Enables this module to consume all left over space dynamically. Enables this module to consume all left over space dynamically.
*zero-on-mute*: ++
typeof: bool ++
default: true ++
`true` = The slider will be set to `min` when the source/sink is muted. ++
`false` = The slider will continue to show the unmuted volume level when the source/sink is muted.
*unmute-on-volume-change*: ++
typeof: bool ++
default: true ++
Specifies whether to unmute a muted souce/sink when its volume is changed by the user moving the slider.
# EXAMPLES # EXAMPLES
@@ -41,7 +52,9 @@ The volume can be controlled by dragging the slider across the bar or clicking o
"pulseaudio/slider": { "pulseaudio/slider": {
"min": 0, "min": 0,
"max": 100, "max": 100,
"orientation": "horizontal" "orientation": "horizontal",
"zero-on-mute": false,
"unmute-on-volume-change": false
} }
``` ```
@@ -52,6 +65,9 @@ The slider is a component with multiple CSS Nodes, of which the following are ex
*#pulseaudio-slider*: ++ *#pulseaudio-slider*: ++
Controls the style of the box *around* the slider and bar. Controls the style of the box *around* the slider and bar.
*#pulseaudio-slider.muted*: ++
Controls the style when the audio source/sink is muted.
*#pulseaudio-slider slider*: ++ *#pulseaudio-slider slider*: ++
Controls the style of the slider handle. Controls the style of the slider handle.
@@ -85,4 +101,8 @@ The slider is a component with multiple CSS Nodes, of which the following are ex
border-radius: 5px; border-radius: 5px;
background: green; background: green;
} }
#pulseaudio-slider.muted highlight {
background-color: orange;
}
``` ```
+20 -6
View File
@@ -8,13 +8,27 @@ waybar-styles - using stylesheets for waybar
Waybar uses Cascading Style Sheets (CSS) to configure its appearance. Waybar uses Cascading Style Sheets (CSS) to configure its appearance.
It uses the first file found in this search order: When the system appearance is light, Waybar first looks for *style-light.css*.
When the system appearance is dark, Waybar first looks for *style-dark.css*.
If no appearance-specific stylesheet is found, it falls back to *style.css*.
- *$XDG_CONFIG_HOME/waybar/style.css* Waybar uses the first file found in this search order for each stylesheet name:
- *~/.config/waybar/style.css*
- *~/waybar/style.css* - *$XDG_CONFIG_HOME/waybar/<stylesheet>*
- */etc/xdg/waybar/style.css* - *~/.config/waybar/<stylesheet>*
- *@sysconfdir@/xdg/waybar/style.css* - *~/waybar/<stylesheet>*
- */etc/xdg/waybar/<stylesheet>*
- *@sysconfdir@/xdg/waybar/<stylesheet>*
For example, a light theme is loaded from the first available file among:
- *$XDG_CONFIG_HOME/waybar/style-light.css*
- *~/.config/waybar/style-light.css*
- *~/waybar/style-light.css*
- */etc/xdg/waybar/style-light.css*
- *@sysconfdir@/xdg/waybar/style-light.css*
If no file is found there, Waybar repeats the same search using *style.css*.
# EXAMPLE # EXAMPLE
+6
View File
@@ -27,6 +27,12 @@ Addressed by *temperature*
The path of the hwmon-directory of the device, e.g. */sys/devices/pci0000:00/0000:00:18.3/hwmon*. (Note that the subdirectory *hwmon/hwmon#*, where *#* is a number is not part of the path!) Has to be used together with *input-filename*. The path of the hwmon-directory of the device, e.g. */sys/devices/pci0000:00/0000:00:18.3/hwmon*. (Note that the subdirectory *hwmon/hwmon#*, where *#* is a number is not part of the path!) Has to be used together with *input-filename*.
This can also be an array of strings, for which, it just works like *hwmon-path*. This can also be an array of strings, for which, it just works like *hwmon-path*.
*hwmon-name*: ++
typeof: string ++
Select a hwmon device by its name (from /sys/class/hwmon/\*/name), e.g. *amdgpu*.
Requires *input-filename* to be set.
Cannot be used together with *hwmon-path* or *hwmon-path-abs*.
*input-filename*: ++ *input-filename*: ++
typeof: string ++ typeof: string ++
The temperature filename of your *hwmon-path-abs*, e.g. *temp1_input* The temperature filename of your *hwmon-path-abs*, e.g. *temp1_input*
+5
View File
@@ -57,6 +57,11 @@ Addressed by *wlr/taskbar*
default: false ++ default: false ++
If set to true, group tasks by their app_id. Cannot be used with 'active-first'. If set to true, group tasks by their app_id. Cannot be used with 'active-first'.
*expand*: ++
typeof: bool ++
default: false ++
If set to true, task buttons stretch to fill the available space in the taskbar and long titles are ellipsized to fit. Only takes effect on a horizontal bar; on a vertical bar the buttons keep their content-based size. If set to false, buttons are sized to their content.
*on-click*: ++ *on-click*: ++
typeof: string ++ typeof: string ++
The action which should be triggered when clicking on the application button with the left mouse button. The action which should be triggered when clicking on the application button with the left mouse button.
+2 -1
View File
@@ -19,7 +19,7 @@ Valid locations for this file are:
A good starting point is the default configuration found at https://github.com/Alexays/Waybar/blob/master/resources/config.jsonc A good starting point is the default configuration found at https://github.com/Alexays/Waybar/blob/master/resources/config.jsonc
Also, a minimal example configuration can be found at the bottom of this man page. Also, a minimal example configuration can be found at the bottom of this man page.
The visual display elements for waybar use a CSS stylesheet, see *waybar-styles(5)* for details. The visual display elements for waybar use a CSS stylesheet. Waybar supports *style.css* and the appearance-specific *style-light.css* and *style-dark.css* files; see *waybar-styles(5)* for details.
# BAR CONFIGURATION # BAR CONFIGURATION
@@ -168,6 +168,7 @@ The visual display elements for waybar use a CSS stylesheet, see *waybar-styles(
# MODULE FORMAT # MODULE FORMAT
You can use PangoMarkupFormat (See https://developer.gnome.org/pango/stable/PangoMarkupFormat.html#PangoMarkupFormat). You can use PangoMarkupFormat (See https://developer.gnome.org/pango/stable/PangoMarkupFormat.html#PangoMarkupFormat).
Tooltip appearance is generally controlled globally via CSS and cannot be truly scoped per module. Some modules (such as the clock) provide limited workarounds for customization.
e.g. e.g.
+2 -1
View File
@@ -186,7 +186,8 @@ src_files = files(
'src/util/icon_loader.cpp', 'src/util/icon_loader.cpp',
'src/util/regex_collection.cpp', 'src/util/regex_collection.cpp',
'src/util/css_reload_helper.cpp', 'src/util/css_reload_helper.cpp',
'src/util/transform_8bit_to_rgba.cpp' 'src/util/transform_8bit_to_rgba.cpp',
'src/util/utf8_string.cpp'
) )
man_files = files( man_files = files(
+1 -1
View File
@@ -12,7 +12,7 @@ option('dbusmenu-gtk', type: 'feature', value: 'auto', description: 'Enable supp
option('man-pages', type: 'feature', value: 'auto', description: 'Generate and install man pages') option('man-pages', type: 'feature', value: 'auto', description: 'Generate and install man pages')
option('mpd', type: 'feature', value: 'auto', description: 'Enable support for the Music Player Daemon') option('mpd', type: 'feature', value: 'auto', description: 'Enable support for the Music Player Daemon')
option('rfkill', type: 'feature', value: 'auto', description: 'Enable support for RFKILL') option('rfkill', type: 'feature', value: 'auto', description: 'Enable support for RFKILL')
option('sndio', type: 'feature', value: 'auto', description: 'Enable support for sndio') option('sndio', type: 'feature', value: 'auto', description: 'Enable support for volume control via sndio')
option('logind', type: 'feature', value: 'auto', description: 'Enable support for logind') option('logind', type: 'feature', value: 'auto', description: 'Enable support for logind')
option('tests', type: 'feature', value: 'auto', description: 'Enable tests') option('tests', type: 'feature', value: 'auto', description: 'Enable tests')
option('experimental', type : 'boolean', value : false, description: 'Enable experimental features') option('experimental', type : 'boolean', value : false, description: 'Enable experimental features')
+6 -4
View File
@@ -105,24 +105,26 @@ class PlayerManager:
current_player = self.get_first_playing_player() current_player = self.get_first_playing_player()
if current_player is not None: if current_player is not None:
self.on_metadata_changed(current_player, current_player.props.metadata) self.on_metadata_changed(current_player, current_player.props.metadata)
else: else:
self.clear_output() self.clear_output()
def on_metadata_changed(self, player, metadata, _=None): def on_metadata_changed(self, player, metadata, _=None):
logger.debug(f"Metadata changed for player {player.props.player_name}") logger.debug(f"Metadata changed for player {player.props.player_name}")
player_name = player.props.player_name player_name = player.props.player_name
artist = player.get_artist() artist = player.get_artist()
artist = artist.replace("&", "&amp;") artist = artist and artist.replace("&", "&amp;")
title = player.get_title() title = player.get_title()
title = title.replace("&", "&amp;") title = title and title.replace("&", "&amp;")
track_info = "" track_info = ""
if player_name == "spotify" and "mpris:trackid" in metadata.keys() and ":ad:" in player.props.metadata["mpris:trackid"]: if player_name == "spotify" and "mpris:trackid" in metadata.keys() and ":ad:" in player.props.metadata["mpris:trackid"]:
track_info = "Advertisement" track_info = "Advertisement"
elif artist is not None and title is not None: elif artist is not None and title is not None:
track_info = f"{artist} - {title}" track_info = f"{artist} - {title}"
else: elif title is not None:
track_info = title track_info = title
elif artist is not None:
track_info = artist
if track_info: if track_info:
if player.props.status == "Playing": if player.props.status == "Playing":
+15 -15
View File
@@ -2,27 +2,27 @@
<interface> <interface>
<object class="GtkMenu" id="menu"> <object class="GtkMenu" id="menu">
<child> <child>
<object class="GtkMenuItem" id="suspend"> <object class="GtkMenuItem" id="suspend">
<property name="label">Suspend</property> <property name="label">Suspend</property>
</object> </object>
</child> </child>
<child>
<object class="GtkMenuItem" id="hibernate">
<property name="label">Hibernate</property>
</object>
</child>
<child> <child>
<object class="GtkMenuItem" id="shutdown"> <object class="GtkMenuItem" id="hibernate">
<property name="label">Shutdown</property> <property name="label">Hibernate</property>
</object> </object>
</child>
<child>
<object class="GtkMenuItem" id="shutdown">
<property name="label">Shutdown</property>
</object>
</child> </child>
<child> <child>
<object class="GtkSeparatorMenuItem" id="delimiter1"/> <object class="GtkSeparatorMenuItem" id="delimiter1"/>
</child> </child>
<child> <child>
<object class="GtkMenuItem" id="reboot"> <object class="GtkMenuItem" id="reboot">
<property name="label">Reboot</property> <property name="label">Reboot</property>
</object> </object>
</child> </child>
</object> </object>
</interface> </interface>
+20
View File
@@ -139,6 +139,26 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st
auto ALabel::update() -> void { AModule::update(); } auto ALabel::update() -> void { AModule::update(); }
bool ALabel::setLabelMarkup(const Glib::ustring& markup) {
if (last_label_markup_ == markup) {
return false;
}
label_.set_markup(markup);
last_label_markup_ = markup;
return true;
}
bool ALabel::setTooltipMarkup(const Glib::ustring& markup) {
if (last_tooltip_markup_ == markup) {
return false;
}
label_.set_tooltip_markup(markup);
last_tooltip_markup_ = markup;
return true;
}
std::string ALabel::getIcon(uint16_t percentage, const std::string& alt, uint16_t max) { std::string ALabel::getIcon(uint16_t percentage, const std::string& alt, uint16_t max) {
auto format_icons = config_["format-icons"]; auto format_icons = config_["format-icons"];
if (format_icons.isObject()) { if (format_icons.isObject()) {
+15 -4
View File
@@ -17,10 +17,14 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std::
isTooltip{config_["tooltip"].isBool() ? config_["tooltip"].asBool() : true}, isTooltip{config_["tooltip"].isBool() ? config_["tooltip"].asBool() : true},
isExpand{config_["expand"].isBool() ? config_["expand"].asBool() : false}, isExpand{config_["expand"].isBool() ? config_["expand"].asBool() : false},
distance_scrolled_y_(0.0), distance_scrolled_y_(0.0),
distance_scrolled_x_(0.0) { distance_scrolled_x_(0.0),
cursor_timeout_conn_() {
// Configure module action Map // Configure module action Map
const Json::Value actions{config_["actions"]}; const Json::Value actions{config_["actions"]};
disable_on_sleep_ =
config_["disable-on-sleep"].isBool() ? config_["disable-on-sleep"].asBool() : false;
for (Json::Value::const_iterator it = actions.begin(); it != actions.end(); ++it) { for (Json::Value::const_iterator it = actions.begin(); it != actions.end(); ++it) {
if (it.key().isString() && it->isString()) if (it.key().isString() && it->isString())
if (!eventActionMap_.contains(it.key().asString())) { if (!eventActionMap_.contains(it.key().asString())) {
@@ -72,8 +76,12 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std::
// Respect user configuration of cursor // Respect user configuration of cursor
if (config_.isMember("cursor")) { if (config_.isMember("cursor")) {
if (config_["cursor"].isBool() && config_["cursor"].asBool()) { if (config_["cursor"].isBool()) {
setCursor(Gdk::HAND2); if (config_["cursor"].asBool()) {
setCursor(Gdk::HAND2);
} else {
setCursor(Gdk::ARROW);
}
} else if (config_["cursor"].isInt()) { } else if (config_["cursor"].isInt()) {
setCursor(Gdk::CursorType(config_["cursor"].asInt())); setCursor(Gdk::CursorType(config_["cursor"].asInt()));
} else { } else {
@@ -83,6 +91,9 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std::
} }
AModule::~AModule() { AModule::~AModule() {
if (cursor_timeout_conn_.connected()) {
cursor_timeout_conn_.disconnect();
}
for (const auto& pid : pid_children_) { for (const auto& pid : pid_children_) {
if (pid != -1) { if (pid != -1) {
killpg(pid, SIGTERM); killpg(pid, SIGTERM);
@@ -118,7 +129,7 @@ void AModule::setCursor(Gdk::CursorType const& c) {
} else { } else {
// window may not be accessible yet, in this case, // window may not be accessible yet, in this case,
// schedule another call for setting the cursor in 1 sec // schedule another call for setting the cursor in 1 sec
Glib::signal_timeout().connect_seconds( cursor_timeout_conn_ = Glib::signal_timeout().connect_seconds(
[this, c]() { [this, c]() {
setCursor(c); setCursor(c);
return false; return false;
+67 -3
View File
@@ -172,6 +172,10 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config)
right_.set_spacing(spacing); right_.set_spacing(spacing);
} }
if (config.isMember("height") && !config["height"].isUInt()) {
spdlog::warn("Invalid type for 'height', expected unsigned integer");
}
height_ = config["height"].isUInt() ? config["height"].asUInt() : 0; height_ = config["height"].isUInt() ? config["height"].asUInt() : 0;
width_ = config["width"].isUInt() ? config["width"].asUInt() : 0; width_ = config["width"].isUInt() ? config["width"].asUInt() : 0;
@@ -263,6 +267,16 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config)
window.signal_map_event().connect_notify(sigc::mem_fun(*this, &Bar::onMap)); window.signal_map_event().connect_notify(sigc::mem_fun(*this, &Bar::onMap));
window.signal_unmap().connect([this]() {
spdlog::debug("Output {} unmapped (DPMS off), suspending modules", output->name);
toggleSuspend(true);
});
window.signal_map().connect([this]() {
spdlog::debug("Output {} mapped (DPMS on), resuming modules", output->name);
toggleSuspend(false);
});
#if HAVE_SWAY #if HAVE_SWAY
if (auto ipc = config["ipc"]; ipc.isBool() && ipc.asBool()) { if (auto ipc = config["ipc"]; ipc.isBool() && ipc.asBool()) {
bar_id = Client::inst()->bar_id; bar_id = Client::inst()->bar_id;
@@ -309,6 +323,19 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config)
setupWidgets(); setupWidgets();
window.show_all(); window.show_all();
/*
* If gtk-layer-shell's synchronous wait for the initial configure timed out, show_all() can
* return with a configured but not-yet-presented surface. Kick GTK/layer-shell once control has
* returned to the main loop, when any late initial configure has been dispatched and widgets have
* had a chance to allocate/draw.
*/
Glib::signal_idle().connect(sigc::track_obj([this] {
window.queue_resize();
window.queue_draw();
forceLayerCommit();
return false;
}, *this));
if (spdlog::should_log(spdlog::level::debug)) { if (spdlog::should_log(spdlog::level::debug)) {
// Unfortunately, this function isn't in the C++ bindings, so we have to call the C version. // Unfortunately, this function isn't in the C++ bindings, so we have to call the C version.
char* gtk_tree = gtk_style_context_to_string( char* gtk_tree = gtk_style_context_to_string(
@@ -374,7 +401,14 @@ void waybar::Bar::setMode(const struct bar_mode& mode) {
* gtk-layer-shell schedules a commit on the next frame event in GTK, but this could fail in * gtk-layer-shell schedules a commit on the next frame event in GTK, but this could fail in
* certain scenarios, such as fully occluded bar. * certain scenarios, such as fully occluded bar.
*/ */
gtk_layer_try_force_commit(gtk_window); forceLayerCommit();
}
void waybar::Bar::forceLayerCommit() {
auto* gtk_window = window.gobj();
if (gtk_window != nullptr && gtk_widget_get_realized(GTK_WIDGET(gtk_window))) {
gtk_layer_try_force_commit(gtk_window);
}
wl_display_flush(Client::inst()->wl_display); wl_display_flush(Client::inst()->wl_display);
} }
@@ -545,8 +579,8 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos,
if (group_config["modules"].isNull()) { if (group_config["modules"].isNull()) {
spdlog::warn("Group definition '{}' has not been found, group will be hidden", ref); spdlog::warn("Group definition '{}' has not been found, group will be hidden", ref);
} }
auto group_module = std::make_unique<waybar::Group>( auto group_module =
id_name, class_name, group_config, vertical); std::make_unique<waybar::Group>(id_name, class_name, group_config, vertical);
getModules(factory, ref, group_module.get()); getModules(factory, ref, group_module.get());
module = group_module.release(); module = group_module.release();
@@ -652,6 +686,17 @@ void waybar::Bar::onConfigure(GdkEventConfigure* ev) {
configureGlobalOffset(ev->width, ev->height); configureGlobalOffset(ev->width, ev->height);
spdlog::info(BAR_SIZE_MSG, ev->width, ev->height, output->name); spdlog::info(BAR_SIZE_MSG, ev->width, ev->height, output->name);
/*
* gtk-layer-shell waits for the compositor's initial configure while realizing the window. On a
* busy compositor (common during session startup) that wait can time out even though the initial
* configure arrives shortly afterwards. In that case GTK may not schedule the frame commit that
* presents the first layer-surface buffer, leaving an otherwise configured bar invisible. Force a
* commit after every configure so late initial configures, and later compositor-driven resizes,
* always result in a submitted surface state.
*/
window.queue_draw();
forceLayerCommit();
} }
void waybar::Bar::configureGlobalOffset(int width, int height) { void waybar::Bar::configureGlobalOffset(int width, int height) {
@@ -696,3 +741,22 @@ void waybar::Bar::configureGlobalOffset(int width, int height) {
void waybar::Bar::onOutputGeometryChanged() { void waybar::Bar::onOutputGeometryChanged() {
configureGlobalOffset(window.get_width(), window.get_height()); configureGlobalOffset(window.get_width(), window.get_height());
} }
void waybar::Bar::toggleSuspend(bool suspend) {
auto process_modules = [suspend](Gtk::Box& module_box) {
for (auto* widget : module_box.get_children()) {
auto* module = dynamic_cast<waybar::AModule*>(widget);
if (module && module->shouldSuspend()) {
if (suspend) {
module->suspend();
} else {
module->resume();
}
}
}
};
process_modules(left_);
process_modules(center_);
process_modules(right_);
}
+29 -6
View File
@@ -91,11 +91,14 @@ void waybar::Client::handleOutputDone(void* data, struct zxdg_output_v1* /*xdg_o
output.xdg_output.reset(); output.xdg_output.reset();
spdlog::debug("Output detection done: {} ({})", output.name, output.identifier); spdlog::debug("Output detection done: {} ({})", output.name, output.identifier);
auto configs = client->getOutputConfigs(output); client->pending_outputs_.push_back(&output);
if (!configs.empty()) {
for (const auto& config : configs) { if (!client->bars_scheduled_) {
client->bars.emplace_back(std::make_unique<Bar>(&output, config)); client->bars_scheduled_ = true;
}
Glib::signal_idle().connect_once([client]() {
client->createBarsBatch();
}, Glib::PRIORITY_HIGH_IDLE);
} }
} }
} catch (const std::exception& e) { } catch (const std::exception& e) {
@@ -103,6 +106,25 @@ void waybar::Client::handleOutputDone(void* data, struct zxdg_output_v1* /*xdg_o
} }
} }
void waybar::Client::createBarsBatch() {
pending_outputs_.remove_if([this](auto* output) { return std::none_of(outputs_.begin(), outputs_.end(), [&output](const auto& o) { return &o == output; }); });
for (auto* output : pending_outputs_) {
try {
auto configs = getOutputConfigs(*output);
if (!configs.empty()) {
for (const auto& config : configs) {
bars.emplace_back(std::make_unique<Bar>(output, config));
}
}
} catch (const std::exception& e) {
spdlog::warn("Error creating bar: {}", e.what());
}
}
pending_outputs_.clear();
bars_scheduled_ = false;
}
void waybar::Client::handleOutputName(void* data, struct zxdg_output_v1* /*xdg_output*/, void waybar::Client::handleOutputName(void* data, struct zxdg_output_v1* /*xdg_output*/,
const char* name) { const char* name) {
auto* client = waybar::Client::inst(); auto* client = waybar::Client::inst();
@@ -306,9 +328,10 @@ int waybar::Client::main(int argc, char* argv[]) {
} }
m_cssFile = getStyle(style_opt); m_cssFile = getStyle(style_opt);
setupCss(m_cssFile); setupCss(m_cssFile);
m_cssReloadHelper = std::make_unique<CssReloadHelper>(m_cssFile, [&]() { setupCss(m_cssFile); }); m_cssReloadHelper = std::make_unique<CssReloadHelper>(m_cssFile, [&](const std::string& css_file) { setupCss(css_file); });
portal->signal_appearance_changed().connect([&](waybar::Appearance appearance) { portal->signal_appearance_changed().connect([&](waybar::Appearance appearance) {
auto css_file = getStyle(style_opt, appearance); auto css_file = getStyle(style_opt, appearance);
m_cssReloadHelper->changeCssFile(css_file);
setupCss(css_file); setupCss(css_file);
}); });
+5 -1
View File
@@ -55,9 +55,11 @@ static void catchSignals(waybar::SafeSignal<int>& signal_handler) {
std::signal(SIGINT, writeSignalToPipe); std::signal(SIGINT, writeSignalToPipe);
std::signal(SIGCHLD, writeSignalToPipe); std::signal(SIGCHLD, writeSignalToPipe);
#ifdef SIGRTMIN
for (int sig = SIGRTMIN + 1; sig <= SIGRTMAX; ++sig) { for (int sig = SIGRTMIN + 1; sig <= SIGRTMAX; ++sig) {
std::signal(sig, writeSignalToPipe); std::signal(sig, writeSignalToPipe);
} }
#endif
while (true) { while (true) {
int signum; int signum;
@@ -119,13 +121,15 @@ void handleUserSignal(int signal, bool& reload) {
// If this signal should restart or close the bar, this function will write // If this signal should restart or close the bar, this function will write
// `true` or `false`, respectively, into `reload`. // `true` or `false`, respectively, into `reload`.
static void handleSignalMainThread(int signum, bool& reload) { static void handleSignalMainThread(int signum, bool& reload) {
#ifdef SIGRTMIN
if (signum >= SIGRTMIN + 1 && signum <= SIGRTMAX) { if (signum >= SIGRTMIN + 1 && signum <= SIGRTMAX) {
for (auto& bar : waybar::Client::inst()->bars) { for (auto& bar : waybar::Client::inst()->bars) {
bar->handleSignal(signum); bar->handleSignal(signum);
} }
return; return;
} }
#endif
switch (signum) { switch (signum) {
case SIGUSR1: case SIGUSR1:
handleUserSignal(SIGUSR1, reload); handleUserSignal(SIGUSR1, reload);
+55 -33
View File
@@ -153,7 +153,7 @@ void waybar::modules::Battery::refreshBatteries() {
} }
} }
} catch (fs::filesystem_error& e) { } catch (fs::filesystem_error& e) {
throw std::runtime_error(e.what()); spdlog::warn("Battery directory tracking failed: {}", e.what());
} }
if (warnFirstTime_ && batteries_.empty()) { if (warnFirstTime_ && batteries_.empty()) {
if (config_["bat"].isString()) { if (config_["bat"].isString()) {
@@ -288,63 +288,73 @@ waybar::modules::Battery::getInfos() {
int32_t _current_now_int = 0; int32_t _current_now_int = 0;
bool current_now_exists = false; bool current_now_exists = false;
if (std::ifstream current_now_f{bat / "current_now"}) { if (std::ifstream current_now_f{bat / "current_now"}) {
current_now_exists = true; if (current_now_f >> _current_now_int) {
current_now_f >> _current_now_int; current_now_exists = true;
}
} else if (std::ifstream current_avg_f{bat / "current_avg"}) { } else if (std::ifstream current_avg_f{bat / "current_avg"}) {
current_now_exists = true; if (current_avg_f >> _current_now_int) {
current_avg_f >> _current_now_int; current_now_exists = true;
}
} }
// Documentation ABI allows a negative value when discharging, positive // Documentation ABI allows a negative value when discharging, positive
// value when charging. // value when charging.
current_now = std::abs(_current_now_int); current_now = std::abs(_current_now_int);
if (std::ifstream f{bat / "time_to_empty_now"}) { if (std::ifstream f{bat / "time_to_empty_now"}) {
time_to_empty_now_exists = true; if (f >> time_to_empty_now) {
f >> time_to_empty_now; time_to_empty_now_exists = true;
}
} }
if (std::ifstream f{bat / "time_to_full_now"}) { if (std::ifstream f{bat / "time_to_full_now"}) {
time_to_full_now_exists = true; if (f >> time_to_full_now) {
f >> time_to_full_now; time_to_full_now_exists = true;
}
} }
uint32_t voltage_now = 0; uint32_t voltage_now = 0;
bool voltage_now_exists = false; bool voltage_now_exists = false;
if (std::ifstream voltage_now_f{bat / "voltage_now"}) { if (std::ifstream voltage_now_f{bat / "voltage_now"}) {
voltage_now_exists = true; if (voltage_now_f >> voltage_now) {
voltage_now_f >> voltage_now; voltage_now_exists = true;
}
} else if (std::ifstream voltage_avg_f{bat / "voltage_avg"}) { } else if (std::ifstream voltage_avg_f{bat / "voltage_avg"}) {
voltage_now_exists = true; if (voltage_avg_f >> voltage_now) {
voltage_avg_f >> voltage_now; voltage_now_exists = true;
}
} }
uint32_t charge_full = 0; uint32_t charge_full = 0;
bool charge_full_exists = false; bool charge_full_exists = false;
if (std::ifstream f{bat / "charge_full"}) { if (std::ifstream f{bat / "charge_full"}) {
charge_full_exists = true; if (f >> charge_full) {
f >> charge_full; charge_full_exists = true;
}
} }
uint32_t charge_full_design = 0; uint32_t charge_full_design = 0;
bool charge_full_design_exists = false; bool charge_full_design_exists = false;
if (std::ifstream f{bat / "charge_full_design"}) { if (std::ifstream f{bat / "charge_full_design"}) {
charge_full_design_exists = true; if (f >> charge_full_design) {
f >> charge_full_design; charge_full_design_exists = true;
}
} }
uint32_t charge_now = 0; uint32_t charge_now = 0;
bool charge_now_exists = false; bool charge_now_exists = false;
if (std::ifstream f{bat / "charge_now"}) { if (std::ifstream f{bat / "charge_now"}) {
charge_now_exists = true; if (f >> charge_now) {
f >> charge_now; charge_now_exists = true;
}
} }
uint32_t power_now = 0; uint32_t power_now = 0;
int32_t _power_now_int = 0; int32_t _power_now_int = 0;
bool power_now_exists = false; bool power_now_exists = false;
if (std::ifstream f{bat / "power_now"}) { if (std::ifstream f{bat / "power_now"}) {
power_now_exists = true; if (f >> _power_now_int) {
f >> _power_now_int; power_now_exists = true;
}
} }
// Some drivers (example: Qualcomm) exposes use a negative value when // Some drivers (example: Qualcomm) exposes use a negative value when
// discharging, positive value when charging. // discharging, positive value when charging.
@@ -353,22 +363,25 @@ waybar::modules::Battery::getInfos() {
uint32_t energy_now = 0; uint32_t energy_now = 0;
bool energy_now_exists = false; bool energy_now_exists = false;
if (std::ifstream f{bat / "energy_now"}) { if (std::ifstream f{bat / "energy_now"}) {
energy_now_exists = true; if (f >> energy_now) {
f >> energy_now; energy_now_exists = true;
}
} }
uint32_t energy_full = 0; uint32_t energy_full = 0;
bool energy_full_exists = false; bool energy_full_exists = false;
if (std::ifstream f{bat / "energy_full"}) { if (std::ifstream f{bat / "energy_full"}) {
energy_full_exists = true; if (f >> energy_full) {
f >> energy_full; energy_full_exists = true;
}
} }
uint32_t energy_full_design = 0; uint32_t energy_full_design = 0;
bool energy_full_design_exists = false; bool energy_full_design_exists = false;
if (std::ifstream f{bat / "energy_full_design"}) { if (std::ifstream f{bat / "energy_full_design"}) {
energy_full_design_exists = true; if (f >> energy_full_design) {
f >> energy_full_design; energy_full_design_exists = true;
}
} }
uint16_t cycleCount = 0; uint16_t cycleCount = 0;
@@ -404,8 +417,9 @@ waybar::modules::Battery::getInfos() {
capacity_exists = true; capacity_exists = true;
capacity = 100 * (uint64_t)energy_now / (uint64_t)energy_full; capacity = 100 * (uint64_t)energy_now / (uint64_t)energy_full;
} else if (std::ifstream f{bat / "capacity"}) { } else if (std::ifstream f{bat / "capacity"}) {
capacity_exists = true; if (f >> capacity) {
f >> capacity; capacity_exists = true;
}
} }
if (!voltage_now_exists) { if (!voltage_now_exists) {
@@ -585,11 +599,12 @@ waybar::modules::Battery::getInfos() {
float calculated_capacity{0.0f}; float calculated_capacity{0.0f};
if (total_capacity_exists) { if (total_capacity_exists) {
if (total_capacity > 0.0f) if (total_capacity > 0.0f) {
calculated_capacity = (float)total_capacity / batteries_.size(); calculated_capacity = (float)total_capacity / batteries_.size();
else if (total_energy_full_exists && total_energy_exists) { } else if (total_energy_full_exists && total_energy_exists) {
if (total_energy_full > 0.0f) if (total_energy_full > 0.0f) {
calculated_capacity = ((float)total_energy * 100.0f / (float)total_energy_full); calculated_capacity = ((float)total_energy * 100.0f / (float)total_energy_full);
}
} }
} }
@@ -686,13 +701,20 @@ auto waybar::modules::Battery::update() -> void {
if (status == "Unknown") { if (status == "Unknown") {
status = getAdapterStatus(capacity); status = getAdapterStatus(capacity);
} }
auto state = getState(capacity, true);
bool adapter_online = false;
if (!adapter_.empty()) {
std::ifstream(adapter_ / "online") >> adapter_online;
}
if (config_["full-at-plugged"].asBool() && status == "Full" && adapter_online) {
status = "Plugged";
}
auto status_pretty = status; auto status_pretty = status;
// Transform to lowercase and replace space with dash // Transform to lowercase and replace space with dash
std::ranges::transform(status.begin(), status.end(), status.begin(), std::ranges::transform(status.begin(), status.end(), status.begin(),
[](char ch) { return ch == ' ' ? '-' : std::tolower(ch); }); [](char ch) { return ch == ' ' ? '-' : std::tolower(ch); });
auto format = format_; auto format = format_;
auto state = getState(capacity, true);
processEvents(state, status, capacity); processEvents(state, status, capacity);
setBarClass(state); setBarClass(state);
auto time_remaining_formatted = formatTimeRemaining(time_remaining); auto time_remaining_formatted = formatTimeRemaining(time_remaining);
+6 -1
View File
@@ -402,8 +402,13 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea
data = g_utf8_find_next_char(data, end); data = g_utf8_find_next_char(data, end);
} }
} }
// Note: the stream's default fill character is already a space (L' ' on
// libstdc++'s wide FormatStream, ' ' on libc++'s narrow one), so no
// std::setfill is needed. Passing std::setfill(' ')/std::setfill(L' ')
// here is not portable because the fill char type must match the
// FormatStream's char type, which differs between standard libraries.
os << Glib::ustring::format( os << Glib::ustring::format(
(cldWPos_ != WS::LEFT || line == 0) ? std::left : std::right, std::setfill(L' '), (cldWPos_ != WS::LEFT || line == 0) ? std::left : std::right,
std::setw(cldMonColLen_ + ((line < 2) ? cldWnLen_ - wideCharCount : 0)), std::setw(cldMonColLen_ + ((line < 2) ? cldWnLen_ - wideCharCount : 0)),
calendarLine); calendarLine);
+11 -3
View File
@@ -41,6 +41,9 @@ auto waybar::modules::Cpu::update() -> void {
auto icons = std::vector<std::string>{state}; auto icons = std::vector<std::string>{state};
fmt::dynamic_format_arg_store<fmt::format_context> store; fmt::dynamic_format_arg_store<fmt::format_context> store;
store.push_back(fmt::arg("load", load1)); store.push_back(fmt::arg("load", load1));
store.push_back(fmt::arg("load1", load1));
store.push_back(fmt::arg("load5", load5));
store.push_back(fmt::arg("load15", load15));
store.push_back(fmt::arg("usage", total_usage)); store.push_back(fmt::arg("usage", total_usage));
store.push_back(fmt::arg("icon", getIcon(total_usage, icons))); store.push_back(fmt::arg("icon", getIcon(total_usage, icons)));
store.push_back(fmt::arg("max_frequency", max_frequency)); store.push_back(fmt::arg("max_frequency", max_frequency));
@@ -58,9 +61,14 @@ auto waybar::modules::Cpu::update() -> void {
label_.set_markup(fmt::vformat(format, store)); label_.set_markup(fmt::vformat(format, store));
if (tooltipEnabled()) { if (tooltipEnabled()) {
if (config_["tooltip-format"].isString()) { std::string tooltip_format;
tooltip = config_["tooltip-format"].asString(); if (!state.empty() && config_["tooltip-format-" + state].isString()) {
label_.set_tooltip_markup(fmt::vformat(tooltip, store)); tooltip_format = config_["tooltip-format-" + state].asString();
} else if (config_["tooltip-format"].isString()) {
tooltip_format = config_["tooltip-format"].asString();
}
if (!tooltip_format.empty()) {
label_.set_tooltip_markup(fmt::vformat(tooltip_format, store));
} else { } else {
label_.set_tooltip_markup(tooltip); label_.set_tooltip_markup(tooltip);
} }
+7
View File
@@ -39,6 +39,11 @@ waybar::modules::Custom::~Custom() {
} }
void waybar::modules::Custom::delayWorker() { void waybar::modules::Custom::delayWorker() {
if (!config_["exec"].isString() && !config_["exec-if"].isString()) {
dp.emit();
return;
}
thread_ = [this] { thread_ = [this] {
for (int i : this->pid_children_) { for (int i : this->pid_children_) {
int status; int status;
@@ -138,9 +143,11 @@ void waybar::modules::Custom::waitingWorker() {
} }
void waybar::modules::Custom::refresh(int sig) { void waybar::modules::Custom::refresh(int sig) {
#ifdef SIGRTMIN
if (config_["signal"].isInt() && sig == SIGRTMIN + config_["signal"].asInt()) { if (config_["signal"].isInt() && sig == SIGRTMIN + config_["signal"].asInt()) {
thread_.wake_up(); thread_.wake_up();
} }
#endif
} }
void waybar::modules::Custom::handleEvent() { void waybar::modules::Custom::handleEvent() {
+2 -2
View File
@@ -320,7 +320,7 @@ std::string IPC::buildLuaDispatch(const std::string& dispatcher, const std::stri
// New format: /dispatch hl.dsp.focus({ workspace = "1" }) // New format: /dispatch hl.dsp.focus({ workspace = "1" })
// //
// Old format: dispatch focusworkspaceoncurrentmonitor 2 // Old format: dispatch focusworkspaceoncurrentmonitor 2
// New format: /dispatch hl.dsp.focus({ workspace = "2", monitor = "current" }) // New format: /dispatch hl.dsp.focus({ workspace = "2", on_current_monitor = true })
// //
// Old format: dispatch togglespecialworkspace name // Old format: dispatch togglespecialworkspace name
// New format: /dispatch hl.dsp.workspace.toggle_special("name") // New format: /dispatch hl.dsp.workspace.toggle_special("name")
@@ -329,7 +329,7 @@ std::string IPC::buildLuaDispatch(const std::string& dispatcher, const std::stri
return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\" })"; return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\" })";
} }
if (dispatcher == "focusworkspaceoncurrentmonitor") { if (dispatcher == "focusworkspaceoncurrentmonitor") {
return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\", monitor = \"current\" })"; return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\", on_current_monitor = true })";
} }
if (dispatcher == "togglespecialworkspace") { if (dispatcher == "togglespecialworkspace") {
if (arg.empty()) { if (arg.empty()) {
+37 -5
View File
@@ -48,12 +48,39 @@ auto Language::update() -> void {
fmt::arg("shortDescription", layout_.short_description), fmt::arg("shortDescription", layout_.short_description),
fmt::arg("variant", layout_.variant))); fmt::arg("variant", layout_.variant)));
} }
spdlog::debug("hyprland language formatted layout name {}", layoutName); spdlog::debug("hyprland language formatted layout name {}", layoutName);
std::string tooltipContent = std::string{};
bool tooltip_enabled = tooltipEnabled();
if (tooltip_enabled) {
if (config_.isMember("tooltip-format")) {
auto tooltip_format = config_["tooltip-format"].asString();
if (config_.isMember("tooltip-format-" + layout_.short_description + "-" + layout_.variant)) {
const auto propName = "tooltip-format-" + layout_.short_description + "-" + layout_.variant;
tooltipContent = fmt::format(fmt::runtime(tooltip_format), config_[propName].asString());
} else if (config_.isMember("tooltip-format-" + layout_.short_description)) {
const auto propName = "tooltip-format-" + layout_.short_description;
tooltipContent = fmt::format(fmt::runtime(tooltip_format), config_[propName].asString());
} else {
tooltipContent =
trim(fmt::format(fmt::runtime(tooltip_format), fmt::arg("long", layout_.full_name),
fmt::arg("short", layout_.short_name),
fmt::arg("shortDescription", layout_.short_description),
fmt::arg("variant", layout_.variant)));
}
} else {
// if no tooltip format is provided, use the same text as the module
tooltipContent = layoutName;
}
spdlog::debug("hyprland language formatted tooltip content {}", tooltipContent);
}
if (!format_.empty()) { if (!format_.empty()) {
label_.show(); label_.show();
label_.set_markup(layoutName); label_.set_markup(layoutName);
if (tooltip_enabled) {
label_.set_tooltip_markup(tooltipContent);
}
} else { } else {
label_.hide(); label_.hide();
} }
@@ -74,8 +101,6 @@ void Language::onEvent(const std::string& ev) {
spdlog::warn("hyprland language received malformed event payload: {}", ev); spdlog::warn("hyprland language received malformed event payload: {}", ev);
return; return;
} }
std::string kbName = payload.substr(0, kbSeparator);
// Last comma before variants parenthesis, eg: // Last comma before variants parenthesis, eg:
// activelayout>>micro-star-int'l-co.,-ltd.-msi-gk50-elite-gaming-keyboard,English (US, intl., // activelayout>>micro-star-int'l-co.,-ltd.-msi-gk50-elite-gaming-keyboard,English (US, intl.,
// with dead keys) // with dead keys)
@@ -93,8 +118,15 @@ void Language::onEvent(const std::string& ev) {
} }
auto layoutName = payload.substr(layoutSeparator + 1); auto layoutName = payload.substr(layoutSeparator + 1);
if (config_.isMember("keyboard-name") && kbName != config_["keyboard-name"].asString()) if (config_.isMember("keyboard-name")) {
return; // ignore const auto keyboardName = config_["keyboard-name"].asString();
// The keyboard name itself can contain commas, so match it as a full prefix
// (followed by the ',' separator) rather than comparing against the substring
// before the first comma, which would truncate such names and drop the event.
if (payload.size() <= keyboardName.size() || payload[keyboardName.size()] != ',' ||
payload.compare(0, keyboardName.size(), keyboardName) != 0)
return; // ignore
}
layoutName = waybar::util::sanitize_string(layoutName); layoutName = waybar::util::sanitize_string(layoutName);
+15 -13
View File
@@ -1,5 +1,6 @@
#include "modules/hyprland/window.hpp" #include "modules/hyprland/window.hpp"
#include <fmt/format.h>
#include <glibmm/fileutils.h> #include <glibmm/fileutils.h>
#include <glibmm/keyfile.h> #include <glibmm/keyfile.h>
#include <glibmm/miscutils.h> #include <glibmm/miscutils.h>
@@ -53,8 +54,15 @@ auto Window::update() -> void {
std::string label_text; std::string label_text;
if (!format_.empty()) { if (!format_.empty()) {
label_.show(); label_.show();
// If the focused window name is empty and fallback is configured, use fallback text
std::string displayTitle = windowName;
if (displayTitle.empty() && config_["fallback"].isString()) {
displayTitle = config_["fallback"].asString();
}
label_text = waybar::util::rewriteString( label_text = waybar::util::rewriteString(
fmt::format(fmt::runtime(format_), fmt::arg("title", windowName), fmt::format(fmt::runtime(format_), fmt::arg("title", displayTitle),
fmt::arg("initialTitle", windowData_.initial_title), fmt::arg("initialTitle", windowData_.initial_title),
fmt::arg("class", windowData_.class_name), fmt::arg("class", windowData_.class_name),
fmt::arg("initialClass", windowData_.initial_class_name)), fmt::arg("initialClass", windowData_.initial_class_name)),
@@ -108,21 +116,14 @@ auto Window::update() -> void {
AAppIconLabel::update(); AAppIconLabel::update();
} }
auto Window::getActiveWorkspace() -> Workspace { auto Window::getActiveWorkspace() -> Workspace { return getActiveWorkspace(""); }
const auto workspace = IPC::inst().getSocket1JsonReply("activeworkspace");
if (workspace.isObject()) {
return Workspace::parse(workspace);
}
return {};
}
auto Window::getActiveWorkspace(const std::string& monitorName) -> Workspace { auto Window::getActiveWorkspace(const std::string& monitorName) -> Workspace {
const auto monitors = IPC::inst().getSocket1JsonReply("monitors"); const auto monitors = IPC::inst().getSocket1JsonReply("monitors");
if (monitors.isArray()) { if (monitors.isArray()) {
auto monitor = std::ranges::find_if( auto monitor = std::ranges::find_if(monitors, [&](const Json::Value& monitor) {
monitors, [&](const Json::Value& monitor) { return monitor["name"] == monitorName; }); return monitorName.empty() ? monitor["focused"].asBool() : monitor["name"] == monitorName;
});
if (monitor == std::end(monitors)) { if (monitor == std::end(monitors)) {
spdlog::warn("Monitor not found: {}", monitorName); spdlog::warn("Monitor not found: {}", monitorName);
return Workspace{ return Workspace{
@@ -132,7 +133,8 @@ auto Window::getActiveWorkspace(const std::string& monitorName) -> Workspace {
.last_window_title = "", .last_window_title = "",
}; };
} }
const int id = (*monitor)["activeWorkspace"]["id"].asInt(); const int special_id = (*monitor)["specialWorkspace"]["id"].asInt();
const int id = special_id != 0 ? special_id : (*monitor)["activeWorkspace"]["id"].asInt();
const auto workspaces = IPC::inst().getSocket1JsonReply("workspaces"); const auto workspaces = IPC::inst().getSocket1JsonReply("workspaces");
if (workspaces.isArray()) { if (workspaces.isArray()) {
+147 -13
View File
@@ -1,5 +1,6 @@
#include <json/value.h> #include <json/value.h>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <glibmm/main.h>
#include <memory> #include <memory>
#include <string> #include <string>
@@ -30,6 +31,11 @@ Workspace::Workspace(const Json::Value& workspace_data, Workspaces& workspace_ma
} }
m_button.add_events(Gdk::BUTTON_PRESS_MASK); m_button.add_events(Gdk::BUTTON_PRESS_MASK);
m_button.add_events(Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK);
m_button.signal_enter_notify_event().connect(sigc::mem_fun(*this, &Workspace::handleEnter));
m_button.signal_leave_notify_event().connect(sigc::mem_fun(*this, &Workspace::handleLeave));
m_button.signal_button_press_event().connect(sigc::mem_fun(*this, &Workspace::handleClicked), m_button.signal_button_press_event().connect(sigc::mem_fun(*this, &Workspace::handleClicked),
false); false);
@@ -45,6 +51,12 @@ Workspace::Workspace(const Json::Value& workspace_data, Workspaces& workspace_ma
initializeWindowMap(clients_data); initializeWindowMap(clients_data);
} }
Workspace::~Workspace() {
// Disconnect the hover-check timeout so it can't fire on this destroyed
// instance (Workspaces are removed at runtime while a check may be armed).
stopHoverCheck();
}
void addOrRemoveClass(const Glib::RefPtr<Gtk::StyleContext>& context, bool condition, void addOrRemoveClass(const Glib::RefPtr<Gtk::StyleContext>& context, bool condition,
const std::string& class_name) { const std::string& class_name) {
if (condition) { if (condition) {
@@ -66,6 +78,102 @@ std::optional<WindowRepr> Workspace::closeWindow(WindowAddress const& addr) {
return std::nullopt; return std::nullopt;
} }
bool Workspace::pointerInsideButton() {
auto display = Gdk::Display::get_default();
if (!display) {
return false;
}
auto seat = display->get_default_seat();
if (!seat) {
return false;
}
auto pointer = seat->get_pointer();
if (!pointer) {
return false;
}
Glib::RefPtr<Gdk::Screen> screen;
int pointerRootX = 0;
int pointerRootY = 0;
pointer->get_position(screen, pointerRootX, pointerRootY);
Gtk::Widget* toplevel = m_button.get_toplevel();
if (toplevel == nullptr || !toplevel->get_window()) {
return false;
}
int buttonX = 0;
int buttonY = 0;
if (!m_button.translate_coordinates(*toplevel, 0, 0, buttonX, buttonY)) {
return false;
}
int windowRootX = 0;
int windowRootY = 0;
toplevel->get_window()->get_root_origin(windowRootX, windowRootY);
const auto allocation = m_button.get_allocation();
const int buttonRootX = windowRootX + buttonX;
const int buttonRootY = windowRootY + buttonY;
const int buttonWidth = allocation.get_width();
const int buttonHeight = allocation.get_height();
return pointerRootX >= buttonRootX && pointerRootY >= buttonRootY &&
pointerRootX < buttonRootX + buttonWidth &&
pointerRootY < buttonRootY + buttonHeight;
}
bool Workspace::syncHoverClass() {
auto styleContext = m_button.get_style_context();
if (pointerInsideButton()) {
styleContext->add_class("workspace-hover");
return true;
}
styleContext->remove_class("workspace-hover");
stopHoverCheck();
return false;
}
void Workspace::startHoverCheck() {
if (m_hoverCheckConnection.connected()) {
return;
}
m_hoverCheckConnection = Glib::signal_timeout().connect(
sigc::mem_fun(*this, &Workspace::syncHoverClass),
50);
}
void Workspace::stopHoverCheck() {
if (m_hoverCheckConnection.connected()) {
m_hoverCheckConnection.disconnect();
}
}
bool Workspace::handleEnter(GdkEventCrossing* /*event*/) {
m_button.get_style_context()->add_class("workspace-hover");
startHoverCheck();
return false;
}
bool Workspace::handleLeave(GdkEventCrossing* /*event*/) {
/*
* Do not remove immediately.
* Workspace taskbar children can fire misleading leave events while the
* pointer is still visually inside the workspace button.
*
* The polling check will remove the class once the pointer really leaves.
*/
startHoverCheck();
return false;
}
bool Workspace::handleClicked(GdkEventButton* bt) const { bool Workspace::handleClicked(GdkEventButton* bt) const {
if (bt->type == GDK_BUTTON_PRESS) { if (bt->type == GDK_BUTTON_PRESS) {
try { try {
@@ -115,7 +223,11 @@ void Workspace::setActiveWindow(WindowAddress const& addr) {
} }
auto activeWindowPos = m_workspaceManager.activeWindowPosition(); auto activeWindowPos = m_workspaceManager.activeWindowPosition();
if (activeIdx.has_value() && activeWindowPos != Workspaces::ActiveWindowPosition::NONE) { const bool has_active_window =
activeIdx.has_value() &&
activeWindowPos != Workspaces::ActiveWindowPosition::NONE;
if (has_active_window) {
auto window = std::move(m_windowMap[*activeIdx]); auto window = std::move(m_windowMap[*activeIdx]);
m_windowMap.erase(m_windowMap.begin() + *activeIdx); m_windowMap.erase(m_windowMap.begin() + *activeIdx);
if (activeWindowPos == Workspaces::ActiveWindowPosition::FIRST) { if (activeWindowPos == Workspaces::ActiveWindowPosition::FIRST) {
@@ -130,7 +242,10 @@ void Workspace::insertWindow(WindowCreationPayload create_window_payload) {
if (!create_window_payload.isEmpty(m_workspaceManager)) { if (!create_window_payload.isEmpty(m_workspaceManager)) {
auto repr = create_window_payload.repr(m_workspaceManager); auto repr = create_window_payload.repr(m_workspaceManager);
if (!repr.empty() || m_workspaceManager.enableTaskbar()) { const bool should_display =
!repr.empty() || m_workspaceManager.enableTaskbar();
if (should_display) {
auto addr = create_window_payload.getAddress(); auto addr = create_window_payload.getAddress();
auto it = std::ranges::find_if( auto it = std::ranges::find_if(
m_windowMap, [&addr](const auto& window) { return window.address == addr; }); m_windowMap, [&addr](const auto& window) { return window.address == addr; });
@@ -142,7 +257,7 @@ void Workspace::insertWindow(WindowCreationPayload create_window_payload) {
} }
} }
} }
}; }
bool Workspace::onWindowOpened(WindowCreationPayload const& create_window_payload) { bool Workspace::onWindowOpened(WindowCreationPayload const& create_window_payload) {
if (create_window_payload.getWorkspaceName() == name()) { if (create_window_payload.getWorkspaceName() == name()) {
@@ -161,6 +276,17 @@ std::string& Workspace::selectIcon(std::map<std::string, std::string>& icons_map
} }
} }
if (isActive() && isSpecial()) {
auto activeIconIt = icons_map.find("active:" + name());
if (activeIconIt != icons_map.end()) {
return activeIconIt->second;
}
auto namedIconIt = icons_map.find(name());
if (namedIconIt != icons_map.end()) {
return namedIconIt->second;
}
}
if (isActive()) { if (isActive()) {
auto activeIconIt = icons_map.find("active"); auto activeIconIt = icons_map.find("active");
if (activeIconIt != icons_map.end()) { if (activeIconIt != icons_map.end()) {
@@ -234,6 +360,7 @@ void Workspace::update(const std::string& workspace_icon) {
auto styleContext = m_button.get_style_context(); auto styleContext = m_button.get_style_context();
addOrRemoveClass(styleContext, isActive(), "active"); addOrRemoveClass(styleContext, isActive(), "active");
addOrRemoveClass(styleContext, isSpecial(), "special"); addOrRemoveClass(styleContext, isSpecial(), "special");
addOrRemoveClass(styleContext, isSpecial(), name());
addOrRemoveClass(styleContext, isEmpty(), "empty"); addOrRemoveClass(styleContext, isEmpty(), "empty");
addOrRemoveClass(styleContext, isPersistent(), "persistent"); addOrRemoveClass(styleContext, isPersistent(), "persistent");
addOrRemoveClass(styleContext, isUrgent(), "urgent"); addOrRemoveClass(styleContext, isUrgent(), "urgent");
@@ -270,7 +397,9 @@ void Workspace::update(const std::string& workspace_icon) {
bool Workspace::isEmpty() const { bool Workspace::isEmpty() const {
auto ignore_list = m_workspaceManager.getIgnoredWindows(); auto ignore_list = m_workspaceManager.getIgnoredWindows();
if (ignore_list.empty()) { const bool no_ignore_rules = ignore_list.empty();
if (no_ignore_rules) {
return m_windows == 0; return m_windows == 0;
} }
// If there are windows but they are all ignored, consider the workspace empty // If there are windows but they are all ignored, consider the workspace empty
@@ -301,15 +430,18 @@ void Workspace::updateTaskbar(const std::string& workspace_icon) {
auto window_box = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL); auto window_box = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL);
window_box->set_tooltip_markup(window_repr.window_title); window_box->set_tooltip_markup(window_repr.window_title);
window_box->get_style_context()->add_class("taskbar-window");
auto button = Gtk::manage(new Gtk::Button());
button->set_relief(Gtk::RELIEF_NONE);
button->add(*window_box);
button->get_style_context()->add_class("taskbar-window");
if (window_repr.isActive) { if (window_repr.isActive) {
window_box->get_style_context()->add_class("active"); button->get_style_context()->add_class("active");
} }
auto event_box = Gtk::manage(new Gtk::EventBox());
event_box->add(*window_box);
if (m_workspaceManager.onClickWindow() != "") { if (m_workspaceManager.onClickWindow() != "") {
event_box->signal_button_press_event().connect( button->signal_button_press_event().connect(
sigc::bind(sigc::mem_fun(*this, &Workspace::handleClick), window_repr.address)); sigc::bind(sigc::mem_fun(*this, &Workspace::handleClick), window_repr.address),
false);
} }
auto text_before = fmt::format(fmt::runtime(m_workspaceManager.taskbarFormatBefore()), auto text_before = fmt::format(fmt::runtime(m_workspaceManager.taskbarFormatBefore()),
@@ -334,8 +466,8 @@ void Workspace::updateTaskbar(const std::string& workspace_icon) {
window_box->pack_start(*window_label_after, true, true); window_box->pack_start(*window_label_after, true, true);
} }
m_content.pack_start(*event_box, true, false); m_content.pack_start(*button, true, false);
event_box->show_all(); button->show_all();
}; };
if (m_workspaceManager.taskbarReverseDirection()) { if (m_workspaceManager.taskbarReverseDirection()) {
@@ -349,7 +481,9 @@ void Workspace::updateTaskbar(const std::string& workspace_icon) {
} }
auto formatAfter = m_workspaceManager.formatAfter(); auto formatAfter = m_workspaceManager.formatAfter();
if (!formatAfter.empty()) { const bool has_format_after = !formatAfter.empty();
if (has_format_after) {
m_labelAfter.set_markup(fmt::format(fmt::runtime(formatAfter), fmt::arg("id", id()), m_labelAfter.set_markup(fmt::format(fmt::runtime(formatAfter), fmt::arg("id", id()),
fmt::arg("name", name()), fmt::arg("name", name()),
fmt::arg("icon", workspace_icon))); fmt::arg("icon", workspace_icon)));
+12 -1
View File
@@ -50,7 +50,8 @@ void Workspaces::init() {
if (m_scrollEventConnection_.connected()) { if (m_scrollEventConnection_.connected()) {
m_scrollEventConnection_.disconnect(); m_scrollEventConnection_.disconnect();
} }
if (barScroll()) { bool hasScrollConfig = config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString();
if (barScroll() || hasScrollConfig) {
auto& window = const_cast<Bar&>(m_bar).window; auto& window = const_cast<Bar&>(m_bar).window;
window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
m_scrollEventConnection_ = m_scrollEventConnection_ =
@@ -326,6 +327,10 @@ void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value& c
// 2. the rule's monitor is the current monitor // 2. the rule's monitor is the current monitor
// 3. no monitor is specified in the rule => assume it needs to be persistent on every monitor // 3. no monitor is specified in the rule => assume it needs to be persistent on every monitor
if (allOutputs() || m_bar.output->name == monitor || monitor.empty()) { if (allOutputs() || m_bar.output->name == monitor || monitor.empty()) {
// => skip ignore-workspaces even if its a persistent
if(isWorkspaceIgnored(workspace)) {
continue;
}
// => persistent workspace should be shown on this monitor // => persistent workspace should be shown on this monitor
auto workspaceData = createMonitorWorkspaceData(workspace, m_bar.output->name); auto workspaceData = createMonitorWorkspaceData(workspace, m_bar.output->name);
workspaceData["persistent-rule"] = true; workspaceData["persistent-rule"] = true;
@@ -1188,6 +1193,12 @@ bool Workspaces::handleScroll(GdkEventScroll* e) {
if (gdk_event_get_pointer_emulated((GdkEvent*)e)) { if (gdk_event_get_pointer_emulated((GdkEvent*)e)) {
return false; return false;
} }
// Check for custom scroll commands first; delegate to base class
if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString()) {
return AModule::handleScroll(e);
}
auto dir = AModule::getScrollDir(e); auto dir = AModule::getScrollDir(e);
if (dir == SCROLL_DIR::NONE) { if (dir == SCROLL_DIR::NONE) {
return true; return true;
+2
View File
@@ -46,9 +46,11 @@ void waybar::modules::Image::delayWorker() {
} }
void waybar::modules::Image::refresh(int sig) { void waybar::modules::Image::refresh(int sig) {
#ifdef SIGRTMIN
if (config_["signal"].isInt() && sig == SIGRTMIN + config_["signal"].asInt()) { if (config_["signal"].isInt() && sig == SIGRTMIN + config_["signal"].asInt()) {
thread_.wake_up(); thread_.wake_up();
} }
#endif
} }
auto waybar::modules::Image::update() -> void { auto waybar::modules::Image::update() -> void {
+20 -24
View File
@@ -1,5 +1,18 @@
#include "modules/memory.hpp" #include "modules/memory.hpp"
namespace {
const std::unordered_map<std::string, float> kUnits = {
{"kB", 1.000},
{"kiB", 1.024},
{"MB", 1.000 * 1000.0},
{"MiB", 1.024 * 1024.0},
{"GB", 1.000 * 1000.0 * 1000.0},
{"GiB", 1.024 * 1024.0 * 1024.0},
{"TB", 1.000 * 1000.0 * 1000.0 * 1000.0},
{"TiB", 1.024 * 1024.0 * 1024.0 * 1024.0}
};
}
waybar::modules::Memory::Memory(const std::string& id, const Json::Value& config) waybar::modules::Memory::Memory(const std::string& id, const Json::Value& config)
: ALabel(config, "memory", id, "{}%", 30) { : ALabel(config, "memory", id, "{}%", 30) {
thread_ = [this] { thread_ = [this] {
@@ -8,6 +21,11 @@ waybar::modules::Memory::Memory(const std::string& id, const Json::Value& config
}; };
if (config["unit"].isString()) { if (config["unit"].isString()) {
unit_ = config["unit"].asString(); unit_ = config["unit"].asString();
if (!kUnits.contains(unit_)) {
unit_ = "GiB";
}
} else {
unit_ = "GiB";
} }
} }
@@ -40,7 +58,7 @@ auto waybar::modules::Memory::update() -> void {
used_swap_percentage = 100 * (swaptotal - swapfree) / swaptotal; used_swap_percentage = 100 * (swaptotal - swapfree) / swaptotal;
} }
float divisor = calc_divisor(unit_); float divisor = kUnits.at(unit_);
float total_ram = memtotal / divisor; float total_ram = memtotal / divisor;
float total_swap = swaptotal / divisor; float total_swap = swaptotal / divisor;
float used_ram = (memtotal - memfree) / divisor; float used_ram = (memtotal - memfree) / divisor;
@@ -82,7 +100,7 @@ auto waybar::modules::Memory::update() -> void {
fmt::arg("swapUsed", used_swap), fmt::arg("avail", available_ram), fmt::arg("swapUsed", used_swap), fmt::arg("avail", available_ram),
fmt::arg("swapAvail", available_swap))); fmt::arg("swapAvail", available_swap)));
} else { } else {
label_.set_tooltip_markup(fmt::format("{:.{}f}GiB used", used_ram, 1)); label_.set_tooltip_markup(fmt::format("{:.{}f}{} used", used_ram, 1, unit_));
} }
} }
} else { } else {
@@ -91,25 +109,3 @@ auto waybar::modules::Memory::update() -> void {
// Call parent update // Call parent update
ALabel::update(); ALabel::update();
} }
float waybar::modules::Memory::calc_divisor(const std::string& divisor) {
if (divisor == "kB") {
return 1.0;
} else if (divisor == "kiB") {
return 1.024;
} else if (divisor == "MB") {
return 1.000 * 1000.0;
} else if (divisor == "MiB") {
return 1.024 * 1024.0;
} else if (divisor == "GB") {
return 1.000 * 1000.0 * 1000.0;
} else if (divisor == "GiB") {
return 1.024 * 1024.0 * 1024.0;
} else if (divisor == "TB") {
return 1.000 * 1000.0 * 1000.0 * 1000.0;
} else if (divisor == "TiB") {
return 1.024 * 1024.0 * 1024.0 * 1024.0;
} else { // default to GiB if it is anything that we don't recongnise
return 1.024 * 1024.0 * 1024.0;
}
}
+51 -40
View File
@@ -6,6 +6,7 @@
#include <system_error> #include <system_error>
#include <util/sanitize_str.hpp> #include <util/sanitize_str.hpp>
#include <util/utf8_string.hpp>
using namespace waybar::util; using namespace waybar::util;
#include "modules/mpd/state.hpp" #include "modules/mpd/state.hpp"
@@ -24,7 +25,8 @@ waybar::modules::MPD::MPD(const std::string& id, const Json::Value& config)
timeout_(config_["timeout"].isUInt() ? config_["timeout"].asUInt() * 1'000 : 30'000), timeout_(config_["timeout"].isUInt() ? config_["timeout"].asUInt() * 1'000 : 30'000),
connection_(nullptr, &mpd_connection_free), connection_(nullptr, &mpd_connection_free),
status_(nullptr, &mpd_status_free), status_(nullptr, &mpd_status_free),
song_(nullptr, &mpd_song_free) { song_(nullptr, &mpd_song_free),
ellipsis_(config_["ellipsis"].isString() ? config_["ellipsis"].asString() : "\u2026") {
if (!config_["port"].isNull() && !config_["port"].isUInt()) { if (!config_["port"].isNull() && !config_["port"].isUInt()) {
spdlog::warn("{}: `port` configuration should be an unsigned int", module_name_); spdlog::warn("{}: `port` configuration should be an unsigned int", module_name_);
} }
@@ -51,21 +53,6 @@ auto waybar::modules::MPD::update() -> void {
ALabel::update(); ALabel::update();
} }
void waybar::modules::MPD::queryMPD() {
if (connection_ != nullptr) {
spdlog::trace("{}: fetching state information", module_name_);
try {
fetchState();
spdlog::trace("{}: fetch complete", module_name_);
} catch (std::exception const& e) {
spdlog::error("{}: {}", module_name_, e.what());
state_ = MPD_STATE_UNKNOWN;
}
dp.emit();
}
}
std::string waybar::modules::MPD::getTag(mpd_tag_type type, unsigned idx) const { std::string waybar::modules::MPD::getTag(mpd_tag_type type, unsigned idx) const {
std::string result = std::string result =
config_["unknown-tag"].isString() ? config_["unknown-tag"].asString() : "N/A"; config_["unknown-tag"].isString() ? config_["unknown-tag"].asString() : "N/A";
@@ -98,7 +85,7 @@ void waybar::modules::MPD::setLabel() {
? config_["format-disconnected"].asString() ? config_["format-disconnected"].asString()
: "disconnected"; : "disconnected";
if (format.empty()) { if (format.empty()) {
label_.set_markup(format); setLabelMarkup(format);
label_.show(); label_.show();
} else { } else {
label_.hide(); label_.hide();
@@ -110,22 +97,22 @@ void waybar::modules::MPD::setLabel() {
? config_["tooltip-format-disconnected"].asString() ? config_["tooltip-format-disconnected"].asString()
: "MPD (disconnected)"; : "MPD (disconnected)";
// Nothing to format // Nothing to format
label_.set_tooltip_markup(tooltip_format); setTooltipMarkup(tooltip_format);
} }
return; return;
} }
label_.get_style_context()->remove_class("disconnected"); label_.get_style_context()->remove_class("disconnected");
auto format = format_; auto format = format_;
Glib::ustring artist, album_artist, album, title;
std::string date, filename, uri; std::string date, filename, uri;
int song_pos = 0, queue_length = 0, volume = 0; int song_pos = 0, queue_length = 0, volume = 0;
std::chrono::seconds elapsedTime, totalTime; std::chrono::seconds elapsedTime, totalTime;
std::string stateIcon = ""; std::string stateIcon = "";
bool no_song = song_.get() == nullptr; bool no_song = song_.get() == nullptr;
if (stopped() || no_song) { bool is_stopped = stopped();
if (no_song) spdlog::warn("Bug in mpd: no current song but state is not stopped."); if (is_stopped || no_song) {
if (no_song && !is_stopped) spdlog::warn("mpd: no current song while state is not stopped");
format = format =
config_["format-stopped"].isString() ? config_["format-stopped"].asString() : "stopped"; config_["format-stopped"].isString() ? config_["format-stopped"].asString() : "stopped";
label_.get_style_context()->add_class("stopped"); label_.get_style_context()->add_class("stopped");
@@ -137,18 +124,15 @@ void waybar::modules::MPD::setLabel() {
label_.get_style_context()->add_class("playing"); label_.get_style_context()->add_class("playing");
label_.get_style_context()->remove_class("paused"); label_.get_style_context()->remove_class("paused");
} else if (paused()) { } else if (paused()) {
format = config_["format-paused"].isString() ? config_["format-paused"].asString() if (config_["format-paused"].isString()) {
: config_["format"].asString(); format = config_["format-paused"].asString();
}
label_.get_style_context()->add_class("paused"); label_.get_style_context()->add_class("paused");
label_.get_style_context()->remove_class("playing"); label_.get_style_context()->remove_class("playing");
} }
stateIcon = getStateIcon(); stateIcon = getStateIcon();
artist = sanitize_string(getTag(MPD_TAG_ARTIST));
album_artist = sanitize_string(getTag(MPD_TAG_ALBUM_ARTIST));
album = sanitize_string(getTag(MPD_TAG_ALBUM));
title = sanitize_string(getTag(MPD_TAG_TITLE));
date = sanitize_string(getTag(MPD_TAG_DATE)); date = sanitize_string(getTag(MPD_TAG_DATE));
filename = sanitize_string(getFilename()); filename = sanitize_string(getFilename());
uri = mpd_song_get_uri(song_.get()); uri = mpd_song_get_uri(song_.get());
@@ -170,17 +154,12 @@ void waybar::modules::MPD::setLabel() {
std::string repeatIcon = getOptionIcon("repeat", repeatActivated); std::string repeatIcon = getOptionIcon("repeat", repeatActivated);
bool singleActivated = mpd_status_get_single(status_.get()); bool singleActivated = mpd_status_get_single(status_.get());
std::string singleIcon = getOptionIcon("single", singleActivated); std::string singleIcon = getOptionIcon("single", singleActivated);
if (config_["artist-len"].isInt()) artist = artist.substr(0, config_["artist-len"].asInt());
if (config_["album-artist-len"].isInt())
album_artist = album_artist.substr(0, config_["album-artist-len"].asInt());
if (config_["album-len"].isInt()) album = album.substr(0, config_["album-len"].asInt());
if (config_["title-len"].isInt()) title = title.substr(0, config_["title-len"].asInt());
try { try {
auto text = fmt::format( auto text = fmt::format(
fmt::runtime(format), fmt::arg("artist", artist.raw()), fmt::runtime(format), fmt::arg("artist", getArtistStr(true)),
fmt::arg("albumArtist", album_artist.raw()), fmt::arg("album", album.raw()), fmt::arg("albumArtist", getAlbumArtistStr(true)), fmt::arg("album", getAlbumStr(true)),
fmt::arg("title", title.raw()), fmt::arg("date", date), fmt::arg("volume", volume), fmt::arg("title", getTitleStr(true)), fmt::arg("date", date), fmt::arg("volume", volume),
fmt::arg("elapsedTime", elapsedTime), fmt::arg("totalTime", totalTime), fmt::arg("elapsedTime", elapsedTime), fmt::arg("totalTime", totalTime),
fmt::arg("songPosition", song_pos), fmt::arg("queueLength", queue_length), fmt::arg("songPosition", song_pos), fmt::arg("queueLength", queue_length),
fmt::arg("stateIcon", stateIcon), fmt::arg("consumeIcon", consumeIcon), fmt::arg("stateIcon", stateIcon), fmt::arg("consumeIcon", consumeIcon),
@@ -190,7 +169,7 @@ void waybar::modules::MPD::setLabel() {
label_.hide(); label_.hide();
} else { } else {
label_.show(); label_.show();
label_.set_markup(text); setLabelMarkup(text);
} }
} catch (fmt::format_error const& e) { } catch (fmt::format_error const& e) {
spdlog::warn("mpd: format error: {}", e.what()); spdlog::warn("mpd: format error: {}", e.what());
@@ -202,15 +181,15 @@ void waybar::modules::MPD::setLabel() {
: "MPD (connected)"; : "MPD (connected)";
try { try {
auto tooltip_text = fmt::format( auto tooltip_text = fmt::format(
fmt::runtime(tooltip_format), fmt::arg("artist", artist.raw()), fmt::runtime(tooltip_format), fmt::arg("artist", getArtistStr(false)),
fmt::arg("albumArtist", album_artist.raw()), fmt::arg("album", album.raw()), fmt::arg("albumArtist", getAlbumArtistStr(false)), fmt::arg("album", getAlbumStr(false)),
fmt::arg("title", title.raw()), fmt::arg("date", date), fmt::arg("volume", volume), fmt::arg("title", getTitleStr(false)), fmt::arg("date", date), fmt::arg("volume", volume),
fmt::arg("elapsedTime", elapsedTime), fmt::arg("totalTime", totalTime), fmt::arg("elapsedTime", elapsedTime), fmt::arg("totalTime", totalTime),
fmt::arg("songPosition", song_pos), fmt::arg("queueLength", queue_length), fmt::arg("songPosition", song_pos), fmt::arg("queueLength", queue_length),
fmt::arg("stateIcon", stateIcon), fmt::arg("consumeIcon", consumeIcon), fmt::arg("stateIcon", stateIcon), fmt::arg("consumeIcon", consumeIcon),
fmt::arg("randomIcon", randomIcon), fmt::arg("repeatIcon", repeatIcon), fmt::arg("randomIcon", randomIcon), fmt::arg("repeatIcon", repeatIcon),
fmt::arg("singleIcon", singleIcon), fmt::arg("filename", filename), fmt::arg("uri", uri)); fmt::arg("singleIcon", singleIcon), fmt::arg("filename", filename), fmt::arg("uri", uri));
label_.set_tooltip_markup(tooltip_text); setTooltipMarkup(tooltip_text);
} catch (fmt::format_error const& e) { } catch (fmt::format_error const& e) {
spdlog::warn("mpd: format error (tooltip): {}", e.what()); spdlog::warn("mpd: format error (tooltip): {}", e.what());
} }
@@ -257,6 +236,38 @@ std::string waybar::modules::MPD::getOptionIcon(const std::string& optionName,
} }
} }
std::string waybar::modules::MPD::getArtistStr(bool truncated) const {
std::string artist = getTag(MPD_TAG_ARTIST);
if (truncated && config_["artist-len"].isInt()) {
waybar::util::utf8_truncate(artist, ellipsis_, config_["artist-len"].asInt());
}
return sanitize_string(artist);
}
std::string waybar::modules::MPD::getAlbumArtistStr(bool truncated) const {
std::string album_artist = getTag(MPD_TAG_ALBUM_ARTIST);
if (truncated && config_["album-artist-len"].isInt()) {
waybar::util::utf8_truncate(album_artist, ellipsis_, config_["album-artist-len"].asInt());
}
return sanitize_string(album_artist);
}
std::string waybar::modules::MPD::getAlbumStr(bool truncated) const {
std::string album = getTag(MPD_TAG_ALBUM);
if (truncated && config_["album-len"].isInt()) {
waybar::util::utf8_truncate(album, ellipsis_, config_["album-len"].asInt());
}
return sanitize_string(album);
}
std::string waybar::modules::MPD::getTitleStr(bool truncated) const {
std::string title = getTag(MPD_TAG_TITLE);
if (truncated && config_["title-len"].isInt()) {
waybar::util::utf8_truncate(title, ellipsis_, config_["title-len"].asInt());
}
return sanitize_string(title);
}
static bool isServerUnavailable(const std::error_code& ec) { static bool isServerUnavailable(const std::error_code& ec) {
if (ec.category() == std::system_category()) { if (ec.category() == std::system_category()) {
switch (ec.value()) { switch (ec.value()) {
-1
View File
@@ -152,7 +152,6 @@ bool Playing::on_timer() {
return false; return false;
} }
ctx_->queryMPD();
ctx_->emit(); ctx_->emit();
} catch (std::exception const& e) { } catch (std::exception const& e) {
spdlog::warn("mpd: Playing: error: {}", e.what()); spdlog::warn("mpd: Playing: error: {}", e.what());
+14 -57
View File
@@ -7,6 +7,7 @@
#include <string> #include <string>
#include "util/scope_guard.hpp" #include "util/scope_guard.hpp"
#include "util/utf8_string.hpp"
extern "C" { extern "C" {
#include <playerctl/playerctl.h> #include <playerctl/playerctl.h>
@@ -202,69 +203,22 @@ auto Mpris::getIconFromJson(const Json::Value& icons, const std::string& key) ->
// Wide characters count as two, zero-width characters count as zero // Wide characters count as two, zero-width characters count as zero
// Modifies str in-place (unless width = std::string::npos) // Modifies str in-place (unless width = std::string::npos)
// Returns the total width of the string pre-truncating // Returns the total width of the string pre-truncating
size_t utf8_truncate(std::string& str, size_t width = std::string::npos) {
if (str.length() == 0) return 0;
const gchar* trunc_end = nullptr;
size_t total_width = 0;
for (gchar *data = str.data(), *end = data + str.size(); data != nullptr;) {
gunichar c = g_utf8_get_char_validated(data, end - data);
if (c == -1U || c == -2U) {
// invalid unicode, treat string as ascii
if (width != std::string::npos && str.length() > width) str.resize(width);
return str.length();
} else if (g_unichar_iswide(c)) {
total_width += 2;
} else if (!g_unichar_iszerowidth(c) && c != 0xAD) { // neither zero-width nor soft hyphen
total_width += 1;
}
data = g_utf8_find_next_char(data, end);
if (width != std::string::npos && total_width <= width && !g_unichar_isspace(c))
trunc_end = data;
}
if (trunc_end) str.resize(trunc_end - str.data());
return total_width;
}
size_t utf8_width(const std::string& str) { return utf8_truncate(const_cast<std::string&>(str)); }
void truncate(std::string& s, const std::string& ellipsis, size_t max_len) {
if (max_len == 0) {
s.resize(0);
return;
}
size_t len = utf8_truncate(s, max_len);
if (len > max_len) {
size_t ellipsis_len = utf8_width(ellipsis);
if (max_len >= ellipsis_len) {
if (ellipsis_len) utf8_truncate(s, max_len - ellipsis_len);
s += ellipsis;
} else {
s.resize(0);
}
}
}
auto Mpris::getArtistStr(const PlayerInfo& info, bool truncated) -> std::string { auto Mpris::getArtistStr(const PlayerInfo& info, bool truncated) -> std::string {
auto artist = info.artist.value_or(std::string()); auto artist = info.artist.value_or(std::string());
if (truncated && artist_len_ >= 0) truncate(artist, ellipsis_, artist_len_); if (truncated && artist_len_ >= 0) waybar::util::utf8_truncate(artist, ellipsis_, artist_len_);
return artist; return artist;
} }
auto Mpris::getAlbumStr(const PlayerInfo& info, bool truncated) -> std::string { auto Mpris::getAlbumStr(const PlayerInfo& info, bool truncated) -> std::string {
auto album = info.album.value_or(std::string()); auto album = info.album.value_or(std::string());
if (truncated && album_len_ >= 0) truncate(album, ellipsis_, album_len_); if (truncated && album_len_ >= 0) waybar::util::utf8_truncate(album, ellipsis_, album_len_);
return album; return album;
} }
auto Mpris::getTitleStr(const PlayerInfo& info, bool truncated) -> std::string { auto Mpris::getTitleStr(const PlayerInfo& info, bool truncated) -> std::string {
auto title = info.title.value_or(std::string()); auto title = info.title.value_or(std::string());
if (truncated && title_len_ >= 0) truncate(title, ellipsis_, title_len_); if (truncated && title_len_ >= 0) waybar::util::utf8_truncate(title, ellipsis_, title_len_);
return title; return title;
} }
@@ -292,9 +246,9 @@ auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) ->
// keep position format same as length format // keep position format same as length format
auto position = getPositionStr(info, truncated && truncate_hours_ && length.length() < 6); auto position = getPositionStr(info, truncated && truncate_hours_ && length.length() < 6);
size_t artistLen = utf8_width(artist); size_t artistLen = waybar::util::utf8_width(artist);
size_t albumLen = utf8_width(album); size_t albumLen = waybar::util::utf8_width(album);
size_t titleLen = utf8_width(title); size_t titleLen = waybar::util::utf8_width(title);
size_t lengthLen = length.length(); size_t lengthLen = length.length();
size_t posLen = position.length(); size_t posLen = position.length();
@@ -313,7 +267,7 @@ auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) ->
// Since the first element doesn't present a separator and we don't know a priori which one // Since the first element doesn't present a separator and we don't know a priori which one
// it will be, we add a "virtual separatorLen" to the dynamicLen, since we are adding the // it will be, we add a "virtual separatorLen" to the dynamicLen, since we are adding the
// separatorLen to all the other lengths. // separatorLen to all the other lengths.
size_t separatorLen = utf8_width(dynamic_separator_); size_t separatorLen = waybar::util::utf8_width(dynamic_separator_);
size_t dynamicLen = dynamic_len_ + separatorLen; size_t dynamicLen = dynamic_len_ + separatorLen;
if (showArtist) artistLen += separatorLen; if (showArtist) artistLen += separatorLen;
if (showAlbum) albumLen += separatorLen; if (showAlbum) albumLen += separatorLen;
@@ -775,9 +729,12 @@ auto Mpris::update() -> void {
auto tooltip_text = fmt::format( auto tooltip_text = fmt::format(
fmt::runtime(tooltipstr), fmt::arg("player", info.name), fmt::runtime(tooltipstr), fmt::arg("player", info.name),
fmt::arg("status", info.status_string), fmt::arg("status", info.status_string),
fmt::arg("artist", getArtistStr(info, tooltip_len_limits_)), fmt::arg("artist",
fmt::arg("title", getTitleStr(info, tooltip_len_limits_)), std::string(Glib::Markup::escape_text(getArtistStr(info, tooltip_len_limits_)))),
fmt::arg("album", getAlbumStr(info, tooltip_len_limits_)), fmt::arg("title",
std::string(Glib::Markup::escape_text(getTitleStr(info, tooltip_len_limits_)))),
fmt::arg("album",
std::string(Glib::Markup::escape_text(getAlbumStr(info, tooltip_len_limits_)))),
fmt::arg("length", tooltipLength), fmt::arg("position", tooltipPosition), fmt::arg("length", tooltipLength), fmt::arg("position", tooltipPosition),
fmt::arg("dynamic", getDynamicStr(info, tooltip_len_limits_, false)), fmt::arg("dynamic", getDynamicStr(info, tooltip_len_limits_, false)),
fmt::arg("player_icon", getIconFromJson(config_["player-icons"], info.name)), fmt::arg("player_icon", getIconFromJson(config_["player-icons"], info.name)),
+1 -1
View File
@@ -85,7 +85,7 @@ void Language::doUpdate() {
spdlog::debug("niri language formatted layout name {}", layoutName); spdlog::debug("niri language formatted layout name {}", layoutName);
if (!format_.empty()) { if (!layoutName.empty()) {
label_.show(); label_.show();
label_.set_markup(layoutName); label_.set_markup(layoutName);
} else { } else {
+3 -1
View File
@@ -103,7 +103,9 @@ void Workspaces::doUpdate() {
fmt::arg("output", ws["output"].asString())); fmt::arg("output", ws["output"].asString()));
} }
if (!config_["disable-markup"].asBool()) { if (!config_["disable-markup"].asBool()) {
static_cast<Gtk::Label*>(button.get_children()[0])->set_markup(name); auto* child = gtk_bin_get_child(GTK_BIN(button.gobj()));
if (child != nullptr && GTK_IS_LABEL(child))
gtk_label_set_markup(GTK_LABEL(child), name.c_str());
} else { } else {
button.set_label(name); button.set_label(name);
} }
+2 -3
View File
@@ -71,6 +71,7 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, Gtk::Orientat
auto* item = Gtk::make_managed<PrivacyItem>(module, nodeType, nodePtr, orientation, pos, auto* item = Gtk::make_managed<PrivacyItem>(module, nodeType, nodePtr, orientation, pos,
iconSize, transition_duration); iconSize, transition_duration);
box_.add(*item); box_.add(*item);
modules_.push_back(item);
} }
} }
@@ -148,9 +149,7 @@ auto Privacy::update() -> void {
bool useAudioOut = false; bool useAudioOut = false;
mutex_.lock(); mutex_.lock();
for (Gtk::Widget* widget : box_.get_children()) { for (PrivacyItem* module : modules_) {
auto* module = dynamic_cast<PrivacyItem*>(widget);
if (module == nullptr) continue;
switch (module->privacy_type) { switch (module->privacy_type) {
case util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT: case util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT:
setScreenshare = true; setScreenshare = true;
+32 -54
View File
@@ -10,73 +10,51 @@ PulseaudioSlider::PulseaudioSlider(const std::string& id, const Json::Value& con
if (config_["target"].isString()) { if (config_["target"].isString()) {
std::string target = config_["target"].asString(); std::string target = config_["target"].asString();
if (target == "sink") { if (target == "sink") {
this->target = PulseaudioSliderTarget::Sink; this->target = util::PulseaudioTarget::Sink;
} else if (target == "source") { } else if (target == "source") {
this->target = PulseaudioSliderTarget::Source; this->target = util::PulseaudioTarget::Source;
} }
} }
if (config_["zero-on-mute"].isBool()) {
zero_on_mute = config_["zero-on-mute"].asBool();
}
if (config_["unmute-on-volume-change"].isBool()) {
unmute_on_volume_change = config_["unmute-on-volume-change"].asBool();
}
} }
void PulseaudioSlider::update() { void PulseaudioSlider::update() {
switch (target) { uint16_t display_value = backend->getVolume(target);
case PulseaudioSliderTarget::Sink: bool is_muted = backend->getMuted(target);
if (backend->getSinkMuted()) {
scale_.set_value(min_);
} else {
scale_.set_value(backend->getSinkVolume());
}
break;
case PulseaudioSliderTarget::Source: if (is_muted) {
if (backend->getSourceMuted()) { if (zero_on_mute) {
scale_.set_value(min_); display_value = min_;
} else { }
scale_.set_value(backend->getSourceVolume()); if (!previously_muted) {
} scale_.get_style_context()->add_class("muted");
break; }
} else if (previously_muted) {
scale_.get_style_context()->remove_class("muted");
} }
scale_.set_value(display_value);
previously_muted = is_muted;
} }
void PulseaudioSlider::onValueChanged() { void PulseaudioSlider::onValueChanged() {
bool is_mute = false; uint16_t slider_value = scale_.get_value();
switch (target) { // Avoid setting sink/source to volume 0 if the user muted it via other means.
case PulseaudioSliderTarget::Sink: if (!backend->getMuted(target) || slider_value != 0) {
if (backend->getSinkMuted()) { if (unmute_on_volume_change) {
is_mute = true; backend->unmute(target);
}
break;
case PulseaudioSliderTarget::Source:
if (backend->getSourceMuted()) {
is_mute = true;
}
break;
}
uint16_t volume = scale_.get_value();
if (is_mute) {
// Avoid setting sink/source to volume 0 if the user muted if via another mean.
if (volume == 0) {
return;
}
// If the sink/source is mute, but the user clicked the slider, unmute it!
else {
switch (target) {
case PulseaudioSliderTarget::Sink:
backend->toggleSinkMute(false);
break;
case PulseaudioSliderTarget::Source:
backend->toggleSourceMute(false);
break;
}
} }
backend->changeVolume(slider_value, min_, max_);
} }
backend->changeVolume(volume, min_, max_);
} }
} // namespace waybar::modules } // namespace waybar::modules
+26 -1
View File
@@ -6,6 +6,9 @@
namespace waybar::modules::SNI { namespace waybar::modules::SNI {
static const unsigned RETRY_DELAY_MS = 200;
static const unsigned MAX_RETRIES = 10;
Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar, Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar,
const std::function<void(std::unique_ptr<Item>&)>& on_add, const std::function<void(std::unique_ptr<Item>&)>& on_add,
const std::function<void(std::unique_ptr<Item>&)>& on_remove, const std::function<void(std::unique_ptr<Item>&)>& on_remove,
@@ -22,6 +25,7 @@ Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar,
on_update_(on_update) {} on_update_(on_update) {}
Host::~Host() { Host::~Host() {
retry_connection_.disconnect();
if (bus_name_id_ > 0) { if (bus_name_id_ > 0) {
Gio::DBus::unown_name(bus_name_id_); Gio::DBus::unown_name(bus_name_id_);
bus_name_id_ = 0; bus_name_id_ = 0;
@@ -53,6 +57,8 @@ void Host::nameAppeared(const Glib::RefPtr<Gio::DBus::Connection>& conn, const G
} }
void Host::nameVanished(const Glib::RefPtr<Gio::DBus::Connection>& conn, const Glib::ustring name) { void Host::nameVanished(const Glib::RefPtr<Gio::DBus::Connection>& conn, const Glib::ustring name) {
retry_connection_.disconnect();
retry_count_ = 0;
g_cancellable_cancel(cancellable_); g_cancellable_cancel(cancellable_);
g_clear_object(&cancellable_); g_clear_object(&cancellable_);
g_clear_object(&watcher_); g_clear_object(&watcher_);
@@ -72,11 +78,30 @@ void Host::proxyReady(GObject* src, GAsyncResult* res, gpointer data) {
return; return;
} }
auto host = static_cast<SNI::Host*>(data); auto host = static_cast<SNI::Host*>(data);
host->watcher_ = watcher;
if (error != nullptr) { if (error != nullptr) {
spdlog::error("Host: {}", error->message); spdlog::error("Host: {}", error->message);
g_clear_object(&host->cancellable_);
if (host->retry_count_ >= MAX_RETRIES) {
spdlog::warn("Host: giving up on watcher proxy creation after {} retries", host->retry_count_);
return;
}
host->retry_count_ += 1;
// Store the timeout connection so it is disconnected in ~Host, avoiding a
// use-after-free if the Host is destroyed before the retry fires.
host->retry_connection_ = Glib::signal_timeout().connect(
[host]() {
if (host->watcher_ != nullptr) {
return false;
}
auto conn = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::BUS_TYPE_SESSION);
host->nameAppeared(conn, "org.kde.StatusNotifierWatcher", "");
return false;
},
RETRY_DELAY_MS);
return; return;
} }
host->retry_count_ = 0;
host->watcher_ = watcher;
sn_watcher_call_register_host(host->watcher_, host->object_path_.c_str(), host->cancellable_, sn_watcher_call_register_host(host->watcher_, host->object_path_.c_str(), host->cancellable_,
&Host::registerHost, data); &Host::registerHost, data);
} }
+12 -2
View File
@@ -190,9 +190,17 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) {
} else if (name == "Status") { } else if (name == "Status") {
setStatus(get_variant<Glib::ustring>(value)); setStatus(get_variant<Glib::ustring>(value));
} else if (name == "IconName") { } else if (name == "IconName") {
icon_name = get_variant<std::string>(value); if (has_custom_icon_) {
spdlog::trace("Item '{}': ignoring IconName update, custom icon is set", id);
} else {
icon_name = get_variant<std::string>(value);
}
} else if (name == "IconPixmap") { } else if (name == "IconPixmap") {
icon_pixmap = this->extractPixBuf(value.gobj()); if (has_custom_icon_) {
spdlog::trace("Item '{}': ignoring IconPixmap update, custom icon is set", id);
} else {
icon_pixmap = this->extractPixBuf(value.gobj());
}
} else if (name == "OverlayIconName") { } else if (name == "OverlayIconName") {
overlay_icon_name = get_variant<std::string>(value); overlay_icon_name = get_variant<std::string>(value);
} else if (name == "OverlayIconPixmap") { } else if (name == "OverlayIconPixmap") {
@@ -270,11 +278,13 @@ void Item::setCustomIcon(const std::string& id) {
Glib::RefPtr<Gdk::Pixbuf> custom_pixbuf = Gdk::Pixbuf::create_from_file(custom_icon); Glib::RefPtr<Gdk::Pixbuf> custom_pixbuf = Gdk::Pixbuf::create_from_file(custom_icon);
icon_name = ""; // icon_name has priority over pixmap icon_name = ""; // icon_name has priority over pixmap
icon_pixmap = custom_pixbuf; icon_pixmap = custom_pixbuf;
has_custom_icon_ = true;
} catch (const Glib::Error& e) { } catch (const Glib::Error& e) {
spdlog::error("Failed to load custom icon {}: {}", custom_icon, e.what()); spdlog::error("Failed to load custom icon {}: {}", custom_icon, e.what());
} }
} else { // if file doesn't exist it's most likely an icon_name } else { // if file doesn't exist it's most likely an icon_name
icon_name = custom_icon; icon_name = custom_icon;
has_custom_icon_ = true;
} }
} }
} }
+4 -13
View File
@@ -24,9 +24,6 @@ Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config)
if (config_["spacing"].isUInt()) { if (config_["spacing"].isUInt()) {
box_.set_spacing(config_["spacing"].asUInt()); box_.set_spacing(config_["spacing"].asUInt());
} }
if (config["show-passive-items"].isBool()) {
show_passive_ = config["show-passive-items"].asBool();
}
nb_hosts_ += 1; nb_hosts_ += 1;
if (config_["icons"].isObject()) { if (config_["icons"].isObject()) {
IconManager::instance().setIconsConfig(config_["icons"]); IconManager::instance().setIconsConfig(config_["icons"]);
@@ -42,6 +39,8 @@ void Tray::onAdd(std::unique_ptr<Item>& item) {
} else { } else {
box_.pack_start(item->event_box); box_.pack_start(item->event_box);
} }
item->event_box.signal_show().connect([this] { dp.emit(); });
item->event_box.signal_hide().connect([this] { dp.emit(); });
dp.emit(); dp.emit();
} }
@@ -51,17 +50,9 @@ void Tray::onRemove(std::unique_ptr<Item>& item) {
} }
auto Tray::update() -> void { auto Tray::update() -> void {
// Show tray only when items are available
std::vector<Gtk::Widget*> children = box_.get_children(); std::vector<Gtk::Widget*> children = box_.get_children();
if (show_passive_) { event_box_.set_visible(std::any_of(children.begin(), children.end(),
event_box_.set_visible(!children.empty()); [](Gtk::Widget* child) { return child->get_visible(); }));
} else {
event_box_.set_visible(!std::all_of(children.begin(), children.end(), [](Gtk::Widget* child) {
return child->get_style_context()->has_class("passive");
}));
}
// Call parent update
AModule::update(); AModule::update();
} }
+42 -15
View File
@@ -54,21 +54,28 @@ void Language::onCmd(const struct Ipc::ipc_response& res) {
std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::mutex> lock(mutex_);
auto payload = parser_.parse(res.payload); auto payload = parser_.parse(res.payload);
std::vector<std::string> used_layouts; std::vector<std::string> used_layouts;
// Display current layout of a device with a maximum count of layouts, expecting that all will // Union layout names across every keyboard input so hot-plugged devices contribute their
// be OK // layouts to the map. Track the device with the most layouts to seed the initially displayed
// layout, matching the previous behaviour at startup.
Json::ArrayIndex max_id = 0, max = 0; Json::ArrayIndex max_id = 0, max = 0;
for (Json::ArrayIndex i = 0; i < payload.size(); i++) { for (Json::ArrayIndex i = 0; i < payload.size(); i++) {
auto size = payload[i][XKB_LAYOUT_NAMES_KEY].size(); if (payload[i]["type"].asString() != "keyboard") continue;
if (size > max) { const auto& names = payload[i][XKB_LAYOUT_NAMES_KEY];
max = size; if (names.size() > max) {
max = names.size();
max_id = i; max_id = i;
} }
for (const auto& layout : names) {
const auto name = layout.asString();
if (std::find(used_layouts.begin(), used_layouts.end(), name) == used_layouts.end()) {
used_layouts.push_back(name);
}
}
} }
for (const auto& layout : payload[max_id][XKB_LAYOUT_NAMES_KEY]) { // Rebuild from scratch so init_layouts_map's duplicate-suffix pass doesn't compound across
used_layouts.push_back(layout.asString()); // refreshes (e.g. "us" -> "us1" -> "us11").
} layouts_map_.clear();
init_layouts_map(used_layouts); init_layouts_map(used_layouts);
set_current_layout(payload[max_id][XKB_ACTIVE_LAYOUT_NAME_KEY].asString()); set_current_layout(payload[max_id][XKB_ACTIVE_LAYOUT_NAME_KEY].asString());
dp.emit(); dp.emit();
@@ -82,13 +89,26 @@ void Language::onEvent(const struct Ipc::ipc_response& res) {
return; return;
} }
bool refresh_inputs = false;
try { try {
std::lock_guard<std::mutex> lock(mutex_); {
auto payload = parser_.parse(res.payload)["input"]; std::lock_guard<std::mutex> lock(mutex_);
if (payload["type"].asString() == "keyboard") { auto root = parser_.parse(res.payload);
set_current_layout(payload[XKB_ACTIVE_LAYOUT_NAME_KEY].asString()); auto change = root["change"].asString();
auto payload = root["input"];
if (payload["type"].asString() == "keyboard") {
// A device was added or its keymap changed - the layout set may have grown, so refresh
// layouts_map_ via IPC_GET_INPUTS once we've released mutex_.
refresh_inputs = (change == "added" || change == "xkb_keymap");
set_current_layout(payload[XKB_ACTIVE_LAYOUT_NAME_KEY].asString());
}
dp.emit();
}
// sendCmd is synchronous: it blocks on the IPC reply and then emits signal_cmd on this same
// thread, which lands in onCmd and re-locks mutex_. Must call it with mutex_ released.
if (refresh_inputs) {
ipc_.sendCmd(IPC_GET_INPUTS);
} }
dp.emit();
} catch (const std::exception& e) { } catch (const std::exception& e) {
spdlog::error("Language: {}", e.what()); spdlog::error("Language: {}", e.what());
} }
@@ -125,8 +145,15 @@ auto Language::update() -> void {
} }
auto Language::set_current_layout(const std::string& current_layout) -> void { auto Language::set_current_layout(const std::string& current_layout) -> void {
// Guard against unknown / empty layout names: transient virtual keyboards (e.g. wtype) and
// hot-plugged devices whose layouts haven't made it into the map yet would otherwise blank out
// layout_ via map::operator[]'s default-construct-on-miss.
auto it = layouts_map_.find(current_layout);
if (it == layouts_map_.end()) {
return;
}
label_.get_style_context()->remove_class(layout_.short_name); label_.get_style_context()->remove_class(layout_.short_name);
layout_ = layouts_map_[current_layout]; layout_ = it->second;
label_.get_style_context()->add_class(layout_.short_name); label_.get_style_context()->add_class(layout_.short_name);
} }
+2 -2
View File
@@ -94,12 +94,12 @@ auto Window::update() -> void {
old_app_id_ = app_id_; old_app_id_ = app_id_;
} }
label_.set_markup(waybar::util::rewriteString( setLabelMarkup(waybar::util::rewriteString(
fmt::format(fmt::runtime(format_), fmt::arg("title", window_), fmt::arg("app_id", app_id_), fmt::format(fmt::runtime(format_), fmt::arg("title", window_), fmt::arg("app_id", app_id_),
fmt::arg("shell", shell_), fmt::arg("marks", marks_)), fmt::arg("shell", shell_), fmt::arg("marks", marks_)),
config_["rewrite"])); config_["rewrite"]));
if (tooltipEnabled()) { if (tooltipEnabled()) {
label_.set_tooltip_markup(window_); setTooltipMarkup(window_);
} }
updateAppIcon(); updateAppIcon();
+18 -7
View File
@@ -229,7 +229,10 @@ bool Workspaces::filterButtons() {
auto ws = std::find_if(workspaces_.begin(), workspaces_.end(), auto ws = std::find_if(workspaces_.begin(), workspaces_.end(),
[it](const auto& node) { return node["name"].asString() == it->first; }); [it](const auto& node) { return node["name"].asString() == it->first; });
if (ws == workspaces_.end() || if (ws == workspaces_.end() ||
(!config_["all-outputs"].asBool() && (*ws)["output"].asString() != bar_.output->name)) { ((*ws).isMember("target_output") ? (*ws)["target_output"].asString() != bar_.output->name &&
(*ws)["target_output"].asString() != ""
: !config_["all-outputs"].asBool() &&
(*ws)["output"].asString() != bar_.output->name)) {
it = buttons_.erase(it); it = buttons_.erase(it);
needReorder = true; needReorder = true;
} else { } else {
@@ -329,7 +332,12 @@ auto Workspaces::update() -> void {
} else { } else {
button.get_style_context()->remove_class("current_output"); button.get_style_context()->remove_class("current_output");
} }
std::string output = (*it)["name"].asString(); std::string output;
if (!config_["disable-markup"].asBool()) {
output = g_markup_escape_text((*it)["name"].asString().c_str(), -1);
} else {
output = (*it)["name"].asString();
}
std::string windows = ""; std::string windows = "";
if (config_["window-rewrite"].isObject()) { if (config_["window-rewrite"].isObject()) {
updateWindows((*it), windows); updateWindows((*it), windows);
@@ -369,11 +377,14 @@ Gtk::Button& Workspaces::addButton(const Json::Value& node) {
node["name"].asString(), node["target_output"].asString(), node["name"].asString(), node["target_output"].asString(),
"--no-auto-back-and-forth", node["name"].asString())); "--no-auto-back-and-forth", node["name"].asString()));
} else { } else {
ipc_.sendCmd(IPC_COMMAND, fmt::format("workspace {} \"{}\"", std::string flag = config_["disable-auto-back-and-forth"].asBool()
config_["disable-auto-back-and-forth"].asBool() ? "--no-auto-back-and-forth"
? "--no-auto-back-and-forth" : "";
: "", if (node["num"].asInt() >= 0) {
node["name"].asString())); ipc_.sendCmd(IPC_COMMAND, fmt::format(workspace_switch_number_cmd_, flag, node["num"].asInt()));
} else {
ipc_.sendCmd(IPC_COMMAND, fmt::format(workspace_switch_cmd_, flag, node["name"].asString()));
}
} }
} catch (const std::exception& e) { } catch (const std::exception& e) {
spdlog::error("Workspaces: {}", e.what()); spdlog::error("Workspaces: {}", e.what());
+7 -2
View File
@@ -84,6 +84,9 @@ SystemdFailedUnits::SystemdFailedUnits(const std::string& id, const Json::Value&
} }
} }
if (!user_props_proxy_ && !system_props_proxy_)
throw std::runtime_error("Neither system nor user status is requested.");
updateData(); updateData();
/* Always update for the first time. */ /* Always update for the first time. */
dp.emit(); dp.emit();
@@ -103,7 +106,7 @@ auto SystemdFailedUnits::notify_cb(const Glib::ustring& sender_name,
void SystemdFailedUnits::RequestSystemState() { void SystemdFailedUnits::RequestSystemState() {
auto load = [](const char* kind, Glib::RefPtr<Gio::DBus::Proxy>& proxy) -> std::string { auto load = [](const char* kind, Glib::RefPtr<Gio::DBus::Proxy>& proxy) -> std::string {
try { try {
if (!proxy) return "unknown"; if (!proxy) return "ignored";
auto parameters = Glib::VariantContainerBase( auto parameters = Glib::VariantContainerBase(
g_variant_new("(ss)", "org.freedesktop.systemd1.Manager", "SystemState")); g_variant_new("(ss)", "org.freedesktop.systemd1.Manager", "SystemState"));
Glib::VariantContainerBase data = proxy->call_sync("Get", parameters); Glib::VariantContainerBase data = proxy->call_sync("Get", parameters);
@@ -122,7 +125,9 @@ void SystemdFailedUnits::RequestSystemState() {
system_state_ = load("systemwide", system_props_proxy_); system_state_ = load("systemwide", system_props_proxy_);
user_state_ = load("user", user_props_proxy_); user_state_ = load("user", user_props_proxy_);
if (system_state_ == "running" && user_state_ == "running")
if ((system_state_ == "running" || system_state_ == "ignored") &&
(user_state_ == "running" || user_state_ == "ignored"))
overall_state_ = "ok"; overall_state_ = "ok";
else else
overall_state_ = "degraded"; overall_state_ = "degraded";
+36
View File
@@ -20,6 +20,24 @@ waybar::modules::Temperature::Temperature(const std::string& id, const Json::Val
if (check_set_path(item.asString())) break; if (check_set_path(item.asString())) break;
}; };
auto find_hwmon_by_name = [](const std::string& name) -> std::optional<std::filesystem::path> {
for (const auto& entry : std::filesystem::directory_iterator("/sys/class/hwmon")) {
std::ifstream f(entry.path() / "name");
std::string hwname;
if (f >> hwname && hwname == name) {
return entry.path();
}
}
return std::nullopt;
};
// ensure either hwmon-name OR old paths are used, not both
if (config_["hwmon-name"].isString() &&
(!config_["hwmon-path"].isNull() || !config_["hwmon-path-abs"].isNull())) {
throw std::runtime_error(
"hwmon-name cannot be used together with hwmon-path or hwmon-path-abs");
}
// if hwmon_path is an array, loop to find first valid item // if hwmon_path is an array, loop to find first valid item
traverseAsArray(config_["hwmon-path"], [this](const std::string& path) { traverseAsArray(config_["hwmon-path"], [this](const std::string& path) {
if (!std::filesystem::exists(path)) return false; if (!std::filesystem::exists(path)) return false;
@@ -40,6 +58,20 @@ waybar::modules::Temperature::Temperature(const std::string& id, const Json::Val
}); });
} }
if (file_path_.empty() && config_["hwmon-name"].isString()) {
if (!config_["input-filename"].isString()) {
throw std::runtime_error("hwmon-name requires input-filename to be set");
}
auto hwmon = find_hwmon_by_name(config_["hwmon-name"].asString());
if (!hwmon) {
throw std::runtime_error("hwmon-name '" + config_["hwmon-name"].asString() + "' not found");
}
file_path_ = hwmon->string() + "/" + config_["input-filename"].asString();
}
if (file_path_.empty()) { if (file_path_.empty()) {
auto zone = config_["thermal-zone"].isInt() ? config_["thermal-zone"].asInt() : 0; auto zone = config_["thermal-zone"].isInt() ? config_["thermal-zone"].asInt() : 0;
file_path_ = fmt::format("/sys/class/thermal/thermal_zone{}/temp", zone); file_path_ = fmt::format("/sys/class/thermal/thermal_zone{}/temp", zone);
@@ -155,3 +187,7 @@ bool waybar::modules::Temperature::isCritical(uint16_t temperature_c) {
return config_["critical-threshold"].isInt() && return config_["critical-threshold"].isInt() &&
temperature_c >= config_["critical-threshold"].asInt(); temperature_c >= config_["critical-threshold"].asInt();
} }
void waybar::modules::Temperature::suspend() { thread_.pause(); }
void waybar::modules::Temperature::resume() { thread_.resume(); }
+7 -4
View File
@@ -347,10 +347,13 @@ auto IPC::update_state_handler(const std::string& event, const Json::Value& data
if (event == "output-wset-changed") { if (event == "output-wset-changed") {
// data: { event, new-wset: wset.name, output: id, new-wset-data: wset, output-data: output } // data: { event, new-wset: wset.name, output: id, new-wset-data: wset, output-data: output }
auto& output = state.outputs.at(data["output-data"]["name"].asString()); try {
auto wset_idx = data["new-wset-data"]["index"].asUInt(); auto& output = state.outputs.at(data["output-data"]["name"].asString());
state.wsets.at(wset_idx).output = output; auto wset_idx = data["new-wset-data"]["index"].asUInt();
output.wset_idx = wset_idx; state.wsets.at(wset_idx).output = output;
output.wset_idx = wset_idx;
} catch (const std::exception&) {
}
return; return;
} }
+1
View File
@@ -211,6 +211,7 @@ void waybar::modules::Wireplumber::onMixerChanged(waybar::modules::Wireplumber*
return; return;
} }
} }
return;
} }
spdlog::warn("[{}]: (onMixerChanged: {}) - Object with id {} not found", self->name_, spdlog::warn("[{}]: (onMixerChanged: {}) - Object with id {} not found", self->name_,
+38 -4
View File
@@ -94,9 +94,35 @@ Task::Task(const waybar::Bar& bar, const Json::Value& config, Taskbar* tbar,
button.set_relief(Gtk::RELIEF_NONE); button.set_relief(Gtk::RELIEF_NONE);
content_.add(text_before_); /* When "expand" is enabled the buttons stretch to fill the taskbar and the
content_.add(icon_); * titles ellipsize to fit within the available space. This only makes sense
content_.add(text_after_); * on a horizontal bar; on a vertical bar the box grows along the vertical
* axis, so ellipsizing/forcing width_chars(1) would truncate every title to
* "…". Keep the historical behavior (buttons sized to their content) as the
* default and when the bar is vertical. */
bool expand = config_["expand"].isBool() && config_["expand"].asBool();
bool horizontal = bar.orientation == Gtk::ORIENTATION_HORIZONTAL;
if (expand && horizontal) {
button.set_hexpand(true);
content_.set_hexpand(true);
text_before_.set_ellipsize(Pango::ELLIPSIZE_END);
text_before_.set_single_line_mode(true);
text_before_.set_width_chars(1);
text_before_.set_xalign(0.0);
text_after_.set_ellipsize(Pango::ELLIPSIZE_END);
text_after_.set_single_line_mode(true);
text_after_.set_width_chars(1);
text_after_.set_xalign(0.0);
content_.pack_start(text_before_, true, true, 0);
content_.pack_start(icon_, false, false, 0);
content_.pack_start(text_after_, true, true, 0);
} else {
content_.add(text_before_);
content_.add(icon_);
content_.add(text_after_);
}
content_.show(); content_.show();
button.add(content_); button.add(content_);
@@ -718,7 +744,15 @@ void Taskbar::handle_finished() {
} }
void Taskbar::add_button(Gtk::Button& bt) { void Taskbar::add_button(Gtk::Button& bt) {
box_.pack_start(bt, false, false); /* Only let the buttons expand to fill the taskbar when "expand" is enabled
* and the bar is horizontal (see the Task constructor for details). */
bool expand = config_["expand"].isBool() && config_["expand"].asBool();
bool horizontal = bar_.orientation == Gtk::ORIENTATION_HORIZONTAL;
if (expand && horizontal) {
box_.pack_start(bt, true, true);
} else {
box_.pack_start(bt, false, false);
}
box_.get_style_context()->remove_class("empty"); box_.get_style_context()->remove_class("empty");
} }
+34 -8
View File
@@ -99,15 +99,17 @@ void AudioBackend::contextStateCb(pa_context* c, void* data) {
nullptr, nullptr); nullptr, nullptr);
break; break;
case PA_CONTEXT_FAILED: case PA_CONTEXT_FAILED:
// When pulseaudio server restarts, the connection is "failed". Try to reconnect. if (pa_context_errno(c) != PA_ERR_CONNECTIONREFUSED) {
// pa_threaded_mainloop_lock is already acquired in callback threads. // When pulseaudio server restarts, the connection is "failed". Try to reconnect.
// So there is no need to lock it again. // pa_threaded_mainloop_lock is already acquired in callback threads.
if (backend->context_ != nullptr) { // So there is no need to lock it again.
pa_context_disconnect(backend->context_); if (backend->context_ != nullptr) {
pa_context_unref(backend->context_); pa_context_disconnect(backend->context_);
backend->context_ = nullptr; pa_context_unref(backend->context_);
backend->context_ = nullptr;
}
backend->connectContext();
} }
backend->connectContext();
break; break;
case PA_CONTEXT_CONNECTING: case PA_CONTEXT_CONNECTING:
case PA_CONTEXT_AUTHORIZING: case PA_CONTEXT_AUTHORIZING:
@@ -259,6 +261,14 @@ void AudioBackend::serverInfoCb(pa_context* context, const pa_server_info* i, vo
pa_context_get_source_info_list(context, sourceInfoCb, data); pa_context_get_source_info_list(context, sourceInfoCb, data);
} }
uint16_t AudioBackend::getVolume(PulseaudioTarget target) const {
if (target == PulseaudioTarget::Source) {
return source_volume_;
} else {
return volume_;
}
}
void AudioBackend::changeVolume(uint16_t volume, uint16_t min_volume, uint16_t max_volume) { void AudioBackend::changeVolume(uint16_t volume, uint16_t min_volume, uint16_t max_volume) {
// Early return if context is not ready // Early return if context is not ready
if ((context_ == nullptr) || pa_context_get_state(context_) != PA_CONTEXT_READY) { if ((context_ == nullptr) || pa_context_get_state(context_) != PA_CONTEXT_READY) {
@@ -368,6 +378,14 @@ void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t ma
pa_threaded_mainloop_unlock(mainloop_); pa_threaded_mainloop_unlock(mainloop_);
} }
bool AudioBackend::getMuted(PulseaudioTarget target) const {
if (target == PulseaudioTarget::Source) {
return source_muted_;
} else {
return muted_;
}
}
void AudioBackend::toggleSinkMute() { void AudioBackend::toggleSinkMute() {
if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return; if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return;
muted_ = !muted_; muted_ = !muted_;
@@ -404,6 +422,14 @@ void AudioBackend::toggleSourceMute(bool mute) {
pa_threaded_mainloop_unlock(mainloop_); pa_threaded_mainloop_unlock(mainloop_);
} }
void AudioBackend::unmute(PulseaudioTarget target) {
if (target == PulseaudioTarget::Source) {
toggleSourceMute(false);
} else {
toggleSinkMute(false);
}
}
bool AudioBackend::isBluetooth() { bool AudioBackend::isBluetooth() {
return monitor_.find("a2dp_sink") != std::string::npos || // PulseAudio return monitor_.find("a2dp_sink") != std::string::npos || // PulseAudio
monitor_.find("a2dp-sink") != std::string::npos || // PipeWire monitor_.find("a2dp-sink") != std::string::npos || // PipeWire
+9 -2
View File
@@ -23,7 +23,8 @@ namespace {
const std::regex IMPORT_REGEX(R"(@import\s+(?:url\()?(?:"|')([^"')]+)(?:"|')\)?;)"); const std::regex IMPORT_REGEX(R"(@import\s+(?:url\()?(?:"|')([^"')]+)(?:"|')\)?;)");
} }
waybar::CssReloadHelper::CssReloadHelper(std::string cssFile, std::function<void()> callback) waybar::CssReloadHelper::CssReloadHelper(std::string cssFile,
std::function<void(const std::string&)> callback)
: m_cssFile(std::move(cssFile)), m_callback(std::move(callback)) {} : m_cssFile(std::move(cssFile)), m_callback(std::move(callback)) {}
std::string waybar::CssReloadHelper::getFileContents(const std::string& filename) { std::string waybar::CssReloadHelper::getFileContents(const std::string& filename) {
@@ -88,6 +89,12 @@ void waybar::CssReloadHelper::monitorChanges() {
} }
} }
void waybar::CssReloadHelper::changeCssFile(const std::string& newCssFile) {
m_fileMonitors.clear();
m_cssFile = newCssFile;
monitorChanges();
}
void waybar::CssReloadHelper::handleFileChange(Glib::RefPtr<Gio::File> const& file, void waybar::CssReloadHelper::handleFileChange(Glib::RefPtr<Gio::File> const& file,
Glib::RefPtr<Gio::File> const& other_type, Glib::RefPtr<Gio::File> const& other_type,
Gio::FileMonitorEvent event_type) { Gio::FileMonitorEvent event_type) {
@@ -95,7 +102,7 @@ void waybar::CssReloadHelper::handleFileChange(Glib::RefPtr<Gio::File> const& fi
// fire for one // fire for one
if (event_type == Gio::FileMonitorEvent::FILE_MONITOR_EVENT_CHANGES_DONE_HINT) { if (event_type == Gio::FileMonitorEvent::FILE_MONITOR_EVENT_CHANGES_DONE_HINT) {
spdlog::debug("Reloading style, file changed: {}", file->get_path()); spdlog::debug("Reloading style, file changed: {}", file->get_path());
m_callback(); m_callback(m_cssFile);
} }
} }
+1 -1
View File
@@ -18,7 +18,7 @@ std::string rewriteString(const std::string& value, const Json::Value& rules) {
// malformated regexes will cause an exception. // malformated regexes will cause an exception.
// in this case, log error and try the next rule. // in this case, log error and try the next rule.
const std::regex rule{it.key().asString(), std::regex_constants::icase}; const std::regex rule{it.key().asString(), std::regex_constants::icase};
if (std::regex_match(value, rule)) { if (std::regex_match(res, rule)) {
res = std::regex_replace(res, rule, it->asString()); res = std::regex_replace(res, rule, it->asString());
} }
} catch (const std::regex_error& e) { } catch (const std::regex_error& e) {
+62
View File
@@ -0,0 +1,62 @@
#include <glib.h>
#include <string>
#include <util/utf8_string.hpp>
namespace waybar::util {
namespace {
// Wide characters count as two, zero-width characters count as zero
// Modifies str in-place (unless width = std::string::npos)
// Returns the total width of the string pre-truncating
size_t measure_and_truncate(std::string& str, size_t width = std::string::npos) {
if (str.length() == 0) return 0;
const gchar* trunc_end = nullptr;
size_t total_width = 0;
for (gchar *data = str.data(), *end = data + str.size(); data != nullptr;) {
gunichar c = g_utf8_get_char_validated(data, end - data);
if (c == -1U || c == -2U) {
// invalid unicode, treat string as ascii
if (width != std::string::npos && str.length() > width) str.resize(width);
return str.length();
} else if (g_unichar_iswide(c)) {
total_width += 2;
} else if (!g_unichar_iszerowidth(c) && c != 0xAD) { // neither zero-width nor soft hyphen
total_width += 1;
}
data = g_utf8_find_next_char(data, end);
if (width != std::string::npos && total_width <= width && !g_unichar_isspace(c))
trunc_end = data;
}
if (trunc_end) str.resize(trunc_end - str.data());
return total_width;
}
} // namespace
size_t utf8_width(const std::string& str) {
return measure_and_truncate(const_cast<std::string&>(str));
}
void utf8_truncate(std::string& s, const std::string& ellipsis, size_t max_len) {
if (max_len == 0) {
s.resize(0);
return;
}
size_t len = measure_and_truncate(s, max_len);
if (len > max_len) {
size_t ellipsis_len = utf8_width(ellipsis);
if (max_len >= ellipsis_len) {
if (ellipsis_len) measure_and_truncate(s, max_len - ellipsis_len);
s += ellipsis;
} else {
s.resize(0);
}
}
}
} // namespace waybar::util
+1 -1
View File
@@ -160,7 +160,7 @@ TEST_CASE("buildLuaDispatch focusworkspaceoncurrentmonitor", "[buildLuaDispatch]
IPCTestHelper::buildLuaDispatch("focusworkspaceoncurrentmonitor", "3"); IPCTestHelper::buildLuaDispatch("focusworkspaceoncurrentmonitor", "3");
REQUIRE( REQUIRE(
result == result ==
"/dispatch hl.dsp.focus({ workspace = \"3\", monitor = \"current\" })"); "/dispatch hl.dsp.focus({ workspace = \"3\", on_current_monitor = true })");
} }
TEST_CASE("buildLuaDispatch togglespecialworkspace", "[buildLuaDispatch]") { TEST_CASE("buildLuaDispatch togglespecialworkspace", "[buildLuaDispatch]") {
+1 -1
View File
@@ -10,7 +10,7 @@
class CssReloadHelperTest : public waybar::CssReloadHelper { class CssReloadHelperTest : public waybar::CssReloadHelper {
public: public:
CssReloadHelperTest() : CssReloadHelper("/tmp/waybar_test.css", [this]() { callback(); }) {} CssReloadHelperTest() : CssReloadHelper("/tmp/waybar_test.css", [this](const std::string&) { callback(); }) {}
void callback() { m_callbackCounter++; } void callback() { m_callbackCounter++; }