diff --git a/include/AModule.hpp b/include/AModule.hpp index a338ffe3..f697c012 100644 --- a/include/AModule.hpp +++ b/include/AModule.hpp @@ -25,6 +25,10 @@ class AModule : public IModule { bool expandEnabled() const; + virtual void suspend() {}; + virtual void resume() {}; + bool shouldSuspend() const { return disable_on_sleep_; } + protected: // Don't need to make an object directly // Derived classes are able to use it @@ -48,6 +52,8 @@ class AModule : public IModule { virtual bool handleMouseLeave(GdkEventCrossing* const& ev); virtual bool handleScroll(GdkEventScroll*); virtual bool handleRelease(GdkEventButton* const& ev); + + bool disable_on_sleep_{false}; GObject* menu_ = nullptr; private: @@ -57,6 +63,7 @@ class AModule : public IModule { bool hasUserEvents_; gdouble distance_scrolled_y_; gdouble distance_scrolled_x_; + sigc::connection cursor_timeout_conn_; std::map eventActionMap_; static const inline std::map, std::string> eventMap_{ {std::make_pair(1, GdkEventType::GDK_BUTTON_PRESS), "on-click"}, diff --git a/include/bar.hpp b/include/bar.hpp index 54b332bf..8f23d6e8 100644 --- a/include/bar.hpp +++ b/include/bar.hpp @@ -75,6 +75,8 @@ class Bar : public sigc::trackable { util::KillSignalAction getOnSigusr1Action(); util::KillSignalAction getOnSigusr2Action(); + void toggleSuspend(bool suspend); + struct waybar_output* output; Json::Value config; struct wl_surface* surface; diff --git a/include/modules/hyprland/backend.hpp b/include/modules/hyprland/backend.hpp index a6ebd191..7c6369da 100644 --- a/include/modules/hyprland/backend.hpp +++ b/include/modules/hyprland/backend.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -35,9 +36,22 @@ class IPC { Json::Value getSocket1JsonReply(const std::string& rq); static std::filesystem::path getSocketFolder(const char* instanceSig); + /// Dispatch a Hyprland command. Automatically uses the correct protocol + /// (legacy text or Lua-based) depending on the running Hyprland version. + static std::string dispatch(const std::string& dispatcher, const std::string& arg); + + /// Build a Lua-format dispatch command string. + static std::string buildLuaDispatch(const std::string& dispatcher, const std::string& arg); + protected: static std::filesystem::path socketFolder_; + /// Detect whether the running Hyprland uses the Lua-based IPC protocol. + /// Returns true for Hyprland >= 0.54 (Lua config), false for older versions. + static bool isLuaProtocol(); + + static std::optional s_luaProtocolDetected_; // cached detection result + private: void socketListener(); void parseIPC(const std::string&); diff --git a/include/modules/memory.hpp b/include/modules/memory.hpp index c73ece23..0e405187 100644 --- a/include/modules/memory.hpp +++ b/include/modules/memory.hpp @@ -19,8 +19,6 @@ class Memory : public ALabel { private: void parseMeminfo(); - static float calc_divisor(const std::string& divisor); - std::unordered_map meminfo_; util::SleeperThread thread_; diff --git a/include/modules/mpd/mpd.hpp b/include/modules/mpd/mpd.hpp index 9d81638e..554f2977 100644 --- a/include/modules/mpd/mpd.hpp +++ b/include/modules/mpd/mpd.hpp @@ -33,6 +33,7 @@ class MPD : public ALabel { detail::unique_status status_; mpd_state state_; detail::unique_song song_; + std::string ellipsis_; public: MPD(const std::string&, const Json::Value&); @@ -45,6 +46,10 @@ class MPD : public ALabel { void setLabel(); std::string getStateIcon() 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 bool handlePlayPause(GdkEventButton* const&); @@ -54,7 +59,6 @@ class MPD : public ALabel { void tryConnect(); void checkErrors(mpd_connection* conn); void fetchState(); - void queryMPD(); inline bool stopped() const { return connection_ && state_ == MPD_STATE_STOP; } inline bool playing() const { return connection_ && state_ == MPD_STATE_PLAY; } diff --git a/include/modules/mpd/state.hpp b/include/modules/mpd/state.hpp index 2c9071b4..0624c196 100644 --- a/include/modules/mpd/state.hpp +++ b/include/modules/mpd/state.hpp @@ -197,7 +197,6 @@ class Context { void tryConnect() const; void checkErrors(mpd_connection*) const; void do_update(); - void queryMPD() const; void fetchState() const; constexpr mpd_state state() const; void emit() const; diff --git a/include/modules/mpd/state.inl.hpp b/include/modules/mpd/state.inl.hpp index 895970e6..cf4a4beb 100644 --- a/include/modules/mpd/state.inl.hpp +++ b/include/modules/mpd/state.inl.hpp @@ -1,13 +1,14 @@ #pragma once namespace detail { +using namespace std::literals::chrono_literals; 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_paused() const { return mpd_module_->paused(); } 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 unique_connection& Context::connection() { return mpd_module_->connection_; } 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::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::emit() const { mpd_module_->emit(); } diff --git a/include/modules/privacy/privacy.hpp b/include/modules/privacy/privacy.hpp index 68d2bf19..729fcc6a 100644 --- a/include/modules/privacy/privacy.hpp +++ b/include/modules/privacy/privacy.hpp @@ -28,6 +28,7 @@ class Privacy : public AModule { // Config Gtk::Box box_; + std::vector modules_; uint iconSpacing = 4; uint iconSize = 20; uint transition_duration = 250; diff --git a/include/modules/sni/item.hpp b/include/modules/sni/item.hpp index 74d54f4c..df87bfb7 100644 --- a/include/modules/sni/item.hpp +++ b/include/modules/sni/item.hpp @@ -46,6 +46,7 @@ class Item : public sigc::trackable { std::string title; std::string icon_name; Glib::RefPtr icon_pixmap; + bool has_custom_icon_ = false; Glib::RefPtr icon_theme; std::string overlay_icon_name; Glib::RefPtr overlay_icon_pixmap; diff --git a/include/modules/sni/tray.hpp b/include/modules/sni/tray.hpp index 3d90b3fd..755aa7b3 100644 --- a/include/modules/sni/tray.hpp +++ b/include/modules/sni/tray.hpp @@ -22,7 +22,6 @@ class Tray : public AModule { void queueUpdate(); static inline std::size_t nb_hosts_ = 0; - bool show_passive_ = false; Gtk::Box box_; SNI::Watcher::singleton watcher_; SNI::Host host_; diff --git a/include/modules/sway/workspaces.hpp b/include/modules/sway/workspaces.hpp index 14528841..1f8d8ed0 100644 --- a/include/modules/sway/workspaces.hpp +++ b/include/modules/sway/workspaces.hpp @@ -24,6 +24,7 @@ class Workspaces : public AModule, public sigc::trackable { private: 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_ = R"(workspace {} "{}"; move workspace to output "{}"; workspace {} "{}")"; diff --git a/include/modules/temperature.hpp b/include/modules/temperature.hpp index 918281be..d65ef84d 100644 --- a/include/modules/temperature.hpp +++ b/include/modules/temperature.hpp @@ -14,6 +14,8 @@ class Temperature : public ALabel { Temperature(const std::string&, const Json::Value&); virtual ~Temperature() = default; auto update() -> void override; + void suspend() override; + void resume() override; private: float getTemperature(); diff --git a/include/util/json.hpp b/include/util/json.hpp index 5f756f35..5f45ae92 100644 --- a/include/util/json.hpp +++ b/include/util/json.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #if (FMT_VERSION >= 90000) @@ -26,14 +27,19 @@ class JsonParser { Json::Value root; // 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; // Use local CharReaderBuilder for thread safety - the IPC singleton's // parser can be called concurrently from multiple module threads Json::CharReaderBuilder readerBuilder; - if (!Json::parseFromStream(readerBuilder, jsonStream, &root, &errs)) { + auto reader = std::unique_ptr(readerBuilder.newCharReader()); + if (!reader->parse(json->data(), json->data() + json->size(), &root, &errs)) { throw std::runtime_error("Error parsing JSON: " + errs); } return root; diff --git a/include/util/sleeper_thread.hpp b/include/util/sleeper_thread.hpp index 1f3a372d..9d20f252 100644 --- a/include/util/sleeper_thread.hpp +++ b/include/util/sleeper_thread.hpp @@ -79,6 +79,12 @@ class SleeperThread { auto sleep_for(std::chrono::system_clock::duration dur) { std::unique_lock lk(mutex_); 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(); auto wait_end = max_time_point; auto now = std::chrono::steady_clock::now(); @@ -95,6 +101,12 @@ class SleeperThread { time_point) { std::unique_lock lk(mutex_); 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 signal_.load(std::memory_order_relaxed) || !do_run_.load(std::memory_order_relaxed); }); @@ -122,6 +134,17 @@ class SleeperThread { } } + void pause() { + std::lock_guard lock(mutex_); + is_paused_ = true; + } + + void resume() { + std::lock_guard lock(mutex_); + is_paused_ = false; + condvar_.notify_all(); + } + ~SleeperThread() { connection_.disconnect(); stop(); @@ -137,6 +160,7 @@ class SleeperThread { std::atomic do_run_ = true; std::atomic signal_ = false; sigc::connection connection_; + bool is_paused_{false}; }; } // namespace waybar::util diff --git a/include/util/utf8_string.hpp b/include/util/utf8_string.hpp new file mode 100644 index 00000000..07a04466 --- /dev/null +++ b/include/util/utf8_string.hpp @@ -0,0 +1,7 @@ +#pragma once +#include + +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 diff --git a/man/waybar-hyprland-language.5.scd b/man/waybar-hyprland-language.5.scd index 5a7ba941..b5ce80aa 100644 --- a/man/waybar-hyprland-language.5.scd +++ b/man/waybar-hyprland-language.5.scd @@ -38,6 +38,20 @@ Addressed by *hyprland/language* typeof: array ++ 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-*: ++ + typeof: string ++ + Allows specifying a different tooltip format for each language. The ** should be replaced with the language code. This can be used to provide a custom tooltip for each language. + *expand*: ++ typeof: bool ++ 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 - *#language* diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index 5284ce99..c852f2ba 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -135,6 +135,14 @@ This setting is ignored if *workspace-taskbar.enable* is set to true. 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. +*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*: ++ typeof: array ++ default: [] ++ diff --git a/meson.build b/meson.build index 0c494eb2..e528a6da 100644 --- a/meson.build +++ b/meson.build @@ -186,7 +186,8 @@ src_files = files( 'src/util/icon_loader.cpp', 'src/util/regex_collection.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( diff --git a/src/AAppIconLabel.cpp b/src/AAppIconLabel.cpp index b72906c3..784f30c2 100644 --- a/src/AAppIconLabel.cpp +++ b/src/AAppIconLabel.cpp @@ -33,21 +33,21 @@ std::string toLowerCase(const std::string& input) { std::optional getFileBySuffix(const std::string& dir, const std::string& suffix, bool check_lower_case) { - if (!std::filesystem::exists(dir)) { - return {}; - } - for (const auto& entry : std::filesystem::recursive_directory_iterator(dir)) { - if (entry.is_regular_file()) { - std::string filename = entry.path().filename().string(); - if (filename.size() < suffix.size()) { - continue; - } - if ((filename.compare(filename.size() - suffix.size(), suffix.size(), suffix) == 0) || - (check_lower_case && filename.compare(filename.size() - suffix.size(), suffix.size(), - toLowerCase(suffix)) == 0)) { - return entry.path().string(); + try { + for (const auto& entry : std::filesystem::recursive_directory_iterator(dir)) { + if (entry.is_regular_file()) { + std::string filename = entry.path().filename().string(); + if (filename.size() < suffix.size()) { + continue; + } + if ((filename.compare(filename.size() - suffix.size(), suffix.size(), suffix) == 0) || + (check_lower_case && filename.compare(filename.size() - suffix.size(), suffix.size(), + toLowerCase(suffix)) == 0)) { + return entry.path().string(); + } } } + } catch (const std::filesystem::filesystem_error&) { } return {}; diff --git a/src/AModule.cpp b/src/AModule.cpp index a5ba69d3..353d0dea 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -17,10 +17,14 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: isTooltip{config_["tooltip"].isBool() ? config_["tooltip"].asBool() : true}, isExpand{config_["expand"].isBool() ? config_["expand"].asBool() : false}, distance_scrolled_y_(0.0), - distance_scrolled_x_(0.0) { + distance_scrolled_x_(0.0), + cursor_timeout_conn_() { // Configure module action Map 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) { if (it.key().isString() && it->isString()) 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 if (config_.isMember("cursor")) { - if (config_["cursor"].isBool() && config_["cursor"].asBool()) { - setCursor(Gdk::HAND2); + if (config_["cursor"].isBool()) { + if (config_["cursor"].asBool()) { + setCursor(Gdk::HAND2); + } else { + setCursor(Gdk::ARROW); + } } else if (config_["cursor"].isInt()) { setCursor(Gdk::CursorType(config_["cursor"].asInt())); } else { @@ -83,6 +91,9 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: } AModule::~AModule() { + if (cursor_timeout_conn_.connected()) { + cursor_timeout_conn_.disconnect(); + } for (const auto& pid : pid_children_) { if (pid != -1) { killpg(pid, SIGTERM); @@ -118,7 +129,7 @@ void AModule::setCursor(Gdk::CursorType const& c) { } else { // window may not be accessible yet, in this case, // 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]() { setCursor(c); return false; diff --git a/src/bar.cpp b/src/bar.cpp index 7ff73c4f..44d02c5d 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -263,6 +263,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_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 (auto ipc = config["ipc"]; ipc.isBool() && ipc.asBool()) { bar_id = Client::inst()->bar_id; @@ -545,8 +555,8 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos, if (group_config["modules"].isNull()) { spdlog::warn("Group definition '{}' has not been found, group will be hidden", ref); } - auto group_module = std::make_unique( - id_name, class_name, group_config, vertical); + auto group_module = + std::make_unique(id_name, class_name, group_config, vertical); getModules(factory, ref, group_module.get()); module = group_module.release(); @@ -696,3 +706,22 @@ void waybar::Bar::configureGlobalOffset(int width, int height) { void waybar::Bar::onOutputGeometryChanged() { 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(widget); + if (module && module->shouldSuspend()) { + if (suspend) { + module->suspend(); + } else { + module->resume(); + } + } + } + }; + + process_modules(left_); + process_modules(center_); + process_modules(right_); +} diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index d39c4920..6d192bda 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -117,17 +117,16 @@ void waybar::modules::Battery::refreshBatteries() { if (((bat_defined && dir_name == config_["bat"].asString()) || !bat_defined) && (fs::exists(node.path() / "capacity") || fs::exists(node.path() / "charge_now")) && fs::exists(node.path() / "uevent") && - (fs::exists(node.path() / "status") || bat_compatibility) && - fs::exists(node.path() / "type")) { + (fs::exists(node.path() / "status") || bat_compatibility)) { std::string type; - std::ifstream(node.path() / "type") >> type; - - if (!type.compare("Battery")) { + if (std::ifstream{node.path() / "type"} >> type && !type.compare("Battery")) { // Ignore non-system power supplies unless explicitly requested - if (!bat_defined && fs::exists(node.path() / "scope")) { + if (!bat_defined) { std::string scope; - std::ifstream(node.path() / "scope") >> scope; - if (g_ascii_strcasecmp(scope.data(), "device") == 0) { + // for hotplug-in device, access it is always unstable because you may remove the + // device anytime so just allow failure happen and do nothing + if (std::ifstream{node.path() / "scope"} >> scope && + g_ascii_strcasecmp(scope.data(), "device") == 0) { continue; } } @@ -154,7 +153,7 @@ void waybar::modules::Battery::refreshBatteries() { } } } catch (fs::filesystem_error& e) { - throw std::runtime_error(e.what()); + spdlog::warn("Battery directory tracking failed: {}", e.what()); } if (warnFirstTime_ && batteries_.empty()) { if (config_["bat"].isString()) { @@ -275,11 +274,11 @@ waybar::modules::Battery::getInfos() { auto bat = item.first; std::string _status; - /* Check for adapter status if battery is not available */ - if (!std::ifstream(bat / "status")) { - std::getline(std::ifstream(adapter_ / "status"), _status); - } else { - std::getline(std::ifstream(bat / "status"), _status); + { + std::ifstream f{bat / "status"}; + if (!std::getline(f, _status)) { + std::getline(std::ifstream(adapter_ / "status"), _status); + } } // Some battery will report current and charge in μA/μAh. @@ -288,64 +287,74 @@ waybar::modules::Battery::getInfos() { uint32_t current_now = 0; int32_t _current_now_int = 0; bool current_now_exists = false; - if (fs::exists(bat / "current_now")) { - current_now_exists = true; - std::ifstream(bat / "current_now") >> _current_now_int; - } else if (fs::exists(bat / "current_avg")) { - current_now_exists = true; - std::ifstream(bat / "current_avg") >> _current_now_int; + if (std::ifstream current_now_f{bat / "current_now"}) { + if (current_now_f >> _current_now_int) { + current_now_exists = true; + } + } else if (std::ifstream current_avg_f{bat / "current_avg"}) { + if (current_avg_f >> _current_now_int) { + current_now_exists = true; + } } // Documentation ABI allows a negative value when discharging, positive // value when charging. current_now = std::abs(_current_now_int); - if (fs::exists(bat / "time_to_empty_now")) { - time_to_empty_now_exists = true; - std::ifstream(bat / "time_to_empty_now") >> time_to_empty_now; + if (std::ifstream f{bat / "time_to_empty_now"}) { + if (f >> time_to_empty_now) { + time_to_empty_now_exists = true; + } } - if (fs::exists(bat / "time_to_full_now")) { - time_to_full_now_exists = true; - std::ifstream(bat / "time_to_full_now") >> time_to_full_now; + if (std::ifstream f{bat / "time_to_full_now"}) { + if (f >> time_to_full_now) { + time_to_full_now_exists = true; + } } uint32_t voltage_now = 0; bool voltage_now_exists = false; - if (fs::exists(bat / "voltage_now")) { - voltage_now_exists = true; - std::ifstream(bat / "voltage_now") >> voltage_now; - } else if (fs::exists(bat / "voltage_avg")) { - voltage_now_exists = true; - std::ifstream(bat / "voltage_avg") >> voltage_now; + if (std::ifstream voltage_now_f{bat / "voltage_now"}) { + if (voltage_now_f >> voltage_now) { + voltage_now_exists = true; + } + } else if (std::ifstream voltage_avg_f{bat / "voltage_avg"}) { + if (voltage_avg_f >> voltage_now) { + voltage_now_exists = true; + } } uint32_t charge_full = 0; bool charge_full_exists = false; - if (fs::exists(bat / "charge_full")) { - charge_full_exists = true; - std::ifstream(bat / "charge_full") >> charge_full; + if (std::ifstream f{bat / "charge_full"}) { + if (f >> charge_full) { + charge_full_exists = true; + } } uint32_t charge_full_design = 0; bool charge_full_design_exists = false; - if (fs::exists(bat / "charge_full_design")) { - charge_full_design_exists = true; - std::ifstream(bat / "charge_full_design") >> charge_full_design; + if (std::ifstream f{bat / "charge_full_design"}) { + if (f >> charge_full_design) { + charge_full_design_exists = true; + } } uint32_t charge_now = 0; bool charge_now_exists = false; - if (fs::exists(bat / "charge_now")) { - charge_now_exists = true; - std::ifstream(bat / "charge_now") >> charge_now; + if (std::ifstream f{bat / "charge_now"}) { + if (f >> charge_now) { + charge_now_exists = true; + } } uint32_t power_now = 0; int32_t _power_now_int = 0; bool power_now_exists = false; - if (fs::exists(bat / "power_now")) { - power_now_exists = true; - std::ifstream(bat / "power_now") >> _power_now_int; + if (std::ifstream f{bat / "power_now"}) { + if (f >> _power_now_int) { + power_now_exists = true; + } } // Some drivers (example: Qualcomm) exposes use a negative value when // discharging, positive value when charging. @@ -353,28 +362,31 @@ waybar::modules::Battery::getInfos() { uint32_t energy_now = 0; bool energy_now_exists = false; - if (fs::exists(bat / "energy_now")) { - energy_now_exists = true; - std::ifstream(bat / "energy_now") >> energy_now; + if (std::ifstream f{bat / "energy_now"}) { + if (f >> energy_now) { + energy_now_exists = true; + } } uint32_t energy_full = 0; bool energy_full_exists = false; - if (fs::exists(bat / "energy_full")) { - energy_full_exists = true; - std::ifstream(bat / "energy_full") >> energy_full; + if (std::ifstream f{bat / "energy_full"}) { + if (f >> energy_full) { + energy_full_exists = true; + } } uint32_t energy_full_design = 0; bool energy_full_design_exists = false; - if (fs::exists(bat / "energy_full_design")) { - energy_full_design_exists = true; - std::ifstream(bat / "energy_full_design") >> energy_full_design; + if (std::ifstream f{bat / "energy_full_design"}) { + if (f >> energy_full_design) { + energy_full_design_exists = true; + } } uint16_t cycleCount = 0; - if (fs::exists(bat / "cycle_count")) { - std::ifstream(bat / "cycle_count") >> cycleCount; + if (std::ifstream f{bat / "cycle_count"}) { + f >> cycleCount; } if (charge_full_design >= largestDesignCapacity) { largestDesignCapacity = charge_full_design; @@ -404,9 +416,10 @@ waybar::modules::Battery::getInfos() { } else if (energy_now_exists && energy_full_exists && energy_full != 0) { capacity_exists = true; capacity = 100 * (uint64_t)energy_now / (uint64_t)energy_full; - } else if (fs::exists(bat / "capacity")) { - capacity_exists = true; - std::ifstream(bat / "capacity") >> capacity; + } else if (std::ifstream f{bat / "capacity"}) { + if (f >> capacity) { + capacity_exists = true; + } } if (!voltage_now_exists) { @@ -586,11 +599,12 @@ waybar::modules::Battery::getInfos() { float calculated_capacity{0.0f}; if (total_capacity_exists) { - if (total_capacity > 0.0f) + if (total_capacity > 0.0f) { calculated_capacity = (float)total_capacity / batteries_.size(); - else if (total_energy_full_exists && total_energy_exists) { - if (total_energy_full > 0.0f) + } else if (total_energy_full_exists && total_energy_exists) { + if (total_energy_full > 0.0f) { calculated_capacity = ((float)total_energy * 100.0f / (float)total_energy_full); + } } } diff --git a/src/modules/cpu_frequency/linux.cpp b/src/modules/cpu_frequency/linux.cpp index 83f06aa5..7b927cdc 100644 --- a/src/modules/cpu_frequency/linux.cpp +++ b/src/modules/cpu_frequency/linux.cpp @@ -23,23 +23,21 @@ std::vector waybar::modules::CpuFrequency::parseCpuFrequencies() { if (frequencies.size() <= 0) { std::string cpufreq_dir = "/sys/devices/system/cpu/cpufreq"; - if (std::filesystem::exists(cpufreq_dir)) { + try { std::vector frequency_files = {"/cpuinfo_min_freq", "/cpuinfo_max_freq"}; for (auto& p : std::filesystem::directory_iterator(cpufreq_dir)) { for (const auto& freq_file : frequency_files) { std::string freq_file_path = p.path().string() + freq_file; - if (std::filesystem::exists(freq_file_path)) { - std::string freq_value; - std::ifstream freq(freq_file_path); - if (freq.is_open()) { - getline(freq, freq_value); - float frequency = std::strtol(freq_value.c_str(), nullptr, 10); - frequencies.push_back(frequency / 1000); - freq.close(); - } + std::string freq_value; + std::ifstream freq(freq_file_path); + if (freq.is_open()) { + getline(freq, freq_value); + float frequency = std::strtol(freq_value.c_str(), nullptr, 10); + frequencies.push_back(frequency / 1000); } } } + } catch (const std::filesystem::filesystem_error&) { } } diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 28def8c9..1d9b320c 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -39,6 +39,11 @@ waybar::modules::Custom::~Custom() { } void waybar::modules::Custom::delayWorker() { + if (!config_["exec"].isString() && !config_["exec-if"].isString()) { + dp.emit(); + return; + } + thread_ = [this] { for (int i : this->pid_children_) { int status; diff --git a/src/modules/hyprland/backend.cpp b/src/modules/hyprland/backend.cpp index d0371202..f38f8216 100644 --- a/src/modules/hyprland/backend.cpp +++ b/src/modules/hyprland/backend.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include "util/scoped_fd.hpp" @@ -20,6 +21,7 @@ namespace waybar::modules::hyprland { std::filesystem::path IPC::socketFolder_; +std::optional IPC::s_luaProtocolDetected_; std::filesystem::path IPC::getSocketFolder(const char* instanceSig) { static std::mutex folderMutex; @@ -290,4 +292,69 @@ Json::Value IPC::getSocket1JsonReply(const std::string& rq) { return parser_.parse(reply); } +bool IPC::isLuaProtocol() { + if (s_luaProtocolDetected_.has_value()) { + return *s_luaProtocolDetected_; + } + + // Probe: send a harmless old-style dispatch and check the error. + // In Lua-based Hyprland (>= 0.54) the error contains "hl.dispatch". + // In older versions it returns "ok" or a different error. + auto reply = getSocket1Reply("dispatch workspace __waybar_probe__"); + bool luaProto = reply.find("hl.dispatch") != std::string::npos; + + if (luaProto) { + spdlog::info("Hyprland IPC: detected Lua-based dispatch protocol (Hyprland >= 0.54)"); + } else { + spdlog::info("Hyprland IPC: detected legacy dispatch protocol"); + } + + s_luaProtocolDetected_ = luaProto; + return luaProto; +} + +std::string IPC::buildLuaDispatch(const std::string& dispatcher, const std::string& arg) { + // Map old-style dispatchers to the new Lua hl.dsp API. + // + // Old format: dispatch workspace 1 + // New format: /dispatch hl.dsp.focus({ workspace = "1" }) + // + // Old format: dispatch focusworkspaceoncurrentmonitor 2 + // New format: /dispatch hl.dsp.focus({ workspace = "2", on_current_monitor = true }) + // + // Old format: dispatch togglespecialworkspace name + // New format: /dispatch hl.dsp.workspace.toggle_special("name") + + if (dispatcher == "workspace") { + return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\" })"; + } + if (dispatcher == "focusworkspaceoncurrentmonitor") { + return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\", on_current_monitor = true })"; + } + if (dispatcher == "togglespecialworkspace") { + if (arg.empty()) { + return "/dispatch hl.dsp.workspace.toggle_special()"; + } + return "/dispatch hl.dsp.workspace.toggle_special(\"" + arg + "\")"; + } + + // Fallback for any other dispatcher: try the old format wrapped in dispatch(). + // This may not work for all dispatchers, but it's a reasonable default. + spdlog::warn("Hyprland IPC: unknown dispatcher '{}' in Lua mode, attempting generic format", + dispatcher); + return "/dispatch hl.dsp." + dispatcher + "(\"" + arg + "\")"; +} + +std::string IPC::dispatch(const std::string& dispatcher, const std::string& arg) { + if (isLuaProtocol()) { + return getSocket1Reply(buildLuaDispatch(dispatcher, arg)); + } + // Legacy format: "dispatch " + std::string cmd = "dispatch " + dispatcher; + if (!arg.empty()) { + cmd += " " + arg; + } + return getSocket1Reply(cmd); +} + } // namespace waybar::modules::hyprland diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index 6e0fe23d..e413f578 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -48,12 +48,39 @@ auto Language::update() -> void { fmt::arg("shortDescription", layout_.short_description), fmt::arg("variant", layout_.variant))); } - 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()) { label_.show(); label_.set_markup(layoutName); + if (tooltip_enabled) { + label_.set_tooltip_markup(tooltipContent); + } } else { label_.hide(); } @@ -74,8 +101,6 @@ void Language::onEvent(const std::string& ev) { spdlog::warn("hyprland language received malformed event payload: {}", ev); return; } - std::string kbName = payload.substr(0, kbSeparator); - // Last comma before variants parenthesis, eg: // activelayout>>micro-star-int'l-co.,-ltd.-msi-gk50-elite-gaming-keyboard,English (US, intl., // with dead keys) @@ -93,8 +118,15 @@ void Language::onEvent(const std::string& ev) { } auto layoutName = payload.substr(layoutSeparator + 1); - if (config_.isMember("keyboard-name") && kbName != config_["keyboard-name"].asString()) - return; // ignore + if (config_.isMember("keyboard-name")) { + 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); diff --git a/src/modules/hyprland/window.cpp b/src/modules/hyprland/window.cpp index 2e3e0bb9..4a855018 100644 --- a/src/modules/hyprland/window.cpp +++ b/src/modules/hyprland/window.cpp @@ -108,21 +108,14 @@ auto Window::update() -> void { AAppIconLabel::update(); } -auto Window::getActiveWorkspace() -> Workspace { - const auto workspace = IPC::inst().getSocket1JsonReply("activeworkspace"); - - if (workspace.isObject()) { - return Workspace::parse(workspace); - } - - return {}; -} +auto Window::getActiveWorkspace() -> Workspace { return getActiveWorkspace(""); } auto Window::getActiveWorkspace(const std::string& monitorName) -> Workspace { const auto monitors = IPC::inst().getSocket1JsonReply("monitors"); if (monitors.isArray()) { - auto monitor = std::ranges::find_if( - monitors, [&](const Json::Value& monitor) { return monitor["name"] == monitorName; }); + auto monitor = std::ranges::find_if(monitors, [&](const Json::Value& monitor) { + return monitorName.empty() ? monitor["focused"].asBool() : monitor["name"] == monitorName; + }); if (monitor == std::end(monitors)) { spdlog::warn("Monitor not found: {}", monitorName); return Workspace{ @@ -132,7 +125,8 @@ auto Window::getActiveWorkspace(const std::string& monitorName) -> Workspace { .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"); if (workspaces.isArray()) { diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index 21e7ef9b..ca427dc9 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -71,20 +71,20 @@ bool Workspace::handleClicked(GdkEventButton* bt) const { try { if (id() > 0) { // normal if (m_workspaceManager.moveToMonitor()) { - m_ipc.getSocket1Reply("dispatch focusworkspaceoncurrentmonitor " + std::to_string(id())); + IPC::dispatch("focusworkspaceoncurrentmonitor", std::to_string(id())); } else { - m_ipc.getSocket1Reply("dispatch workspace " + std::to_string(id())); + IPC::dispatch("workspace", std::to_string(id())); } } else if (!isSpecial()) { // named (this includes persistent) if (m_workspaceManager.moveToMonitor()) { - m_ipc.getSocket1Reply("dispatch focusworkspaceoncurrentmonitor name:" + name()); + IPC::dispatch("focusworkspaceoncurrentmonitor", "name:" + name()); } else { - m_ipc.getSocket1Reply("dispatch workspace name:" + name()); + IPC::dispatch("workspace", "name:" + name()); } } else if (id() != -99) { // named special - m_ipc.getSocket1Reply("dispatch togglespecialworkspace " + name()); + IPC::dispatch("togglespecialworkspace", name()); } else { // special - m_ipc.getSocket1Reply("dispatch togglespecialworkspace"); + IPC::dispatch("togglespecialworkspace", ""); } return true; } catch (const std::exception& e) { @@ -301,15 +301,18 @@ void Workspace::updateTaskbar(const std::string& workspace_icon) { auto window_box = Gtk::make_managed(Gtk::ORIENTATION_HORIZONTAL); 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) { - 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() != "") { - event_box->signal_button_press_event().connect( - sigc::bind(sigc::mem_fun(*this, &Workspace::handleClick), window_repr.address)); + button->signal_button_press_event().connect( + sigc::bind(sigc::mem_fun(*this, &Workspace::handleClick), window_repr.address), + false); } auto text_before = fmt::format(fmt::runtime(m_workspaceManager.taskbarFormatBefore()), @@ -334,8 +337,8 @@ void Workspace::updateTaskbar(const std::string& workspace_icon) { window_box->pack_start(*window_label_after, true, true); } - m_content.pack_start(*event_box, true, false); - event_box->show_all(); + m_content.pack_start(*button, true, false); + button->show_all(); }; if (m_workspaceManager.taskbarReverseDirection()) { diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index f794249b..3fa92920 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -50,7 +50,8 @@ void Workspaces::init() { if (m_scrollEventConnection_.connected()) { m_scrollEventConnection_.disconnect(); } - if (barScroll()) { + bool hasScrollConfig = config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString(); + if (barScroll() || hasScrollConfig) { auto& window = const_cast(m_bar).window; window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); m_scrollEventConnection_ = @@ -326,6 +327,10 @@ void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value& c // 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 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 auto workspaceData = createMonitorWorkspaceData(workspace, m_bar.output->name); workspaceData["persistent-rule"] = true; @@ -1188,6 +1193,12 @@ bool Workspaces::handleScroll(GdkEventScroll* e) { if (gdk_event_get_pointer_emulated((GdkEvent*)e)) { 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); if (dir == SCROLL_DIR::NONE) { return true; @@ -1195,15 +1206,15 @@ bool Workspaces::handleScroll(GdkEventScroll* e) { if (dir == SCROLL_DIR::DOWN || dir == SCROLL_DIR::RIGHT) { if (allOutputs()) { - m_ipc.getSocket1Reply("dispatch workspace e+1"); + IPC::dispatch("workspace", "e+1"); } else { - m_ipc.getSocket1Reply("dispatch workspace m+1"); + IPC::dispatch("workspace", "m+1"); } } else if (dir == SCROLL_DIR::UP || dir == SCROLL_DIR::LEFT) { if (allOutputs()) { - m_ipc.getSocket1Reply("dispatch workspace e-1"); + IPC::dispatch("workspace", "e-1"); } else { - m_ipc.getSocket1Reply("dispatch workspace m-1"); + IPC::dispatch("workspace", "m-1"); } } diff --git a/src/modules/memory/common.cpp b/src/modules/memory/common.cpp index 731f0b08..d41b4fc2 100644 --- a/src/modules/memory/common.cpp +++ b/src/modules/memory/common.cpp @@ -1,5 +1,18 @@ #include "modules/memory.hpp" +namespace { +const std::unordered_map 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) : ALabel(config, "memory", id, "{}%", 30) { thread_ = [this] { @@ -8,6 +21,11 @@ waybar::modules::Memory::Memory(const std::string& id, const Json::Value& config }; if (config["unit"].isString()) { 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; } - float divisor = calc_divisor(unit_); + float divisor = kUnits.at(unit_); float total_ram = memtotal / divisor; float total_swap = swaptotal / 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("swapAvail", available_swap))); } 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 { @@ -91,25 +109,3 @@ auto waybar::modules::Memory::update() -> void { // Call parent 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; - } -} diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 9874f458..00179ae9 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -6,6 +6,7 @@ #include #include +#include using namespace waybar::util; #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), connection_(nullptr, &mpd_connection_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()) { spdlog::warn("{}: `port` configuration should be an unsigned int", module_name_); } @@ -51,21 +53,6 @@ auto waybar::modules::MPD::update() -> void { 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 result = config_["unknown-tag"].isString() ? config_["unknown-tag"].asString() : "N/A"; @@ -117,15 +104,15 @@ void waybar::modules::MPD::setLabel() { label_.get_style_context()->remove_class("disconnected"); auto format = format_; - Glib::ustring artist, album_artist, album, title; std::string date, filename, uri; int song_pos = 0, queue_length = 0, volume = 0; std::chrono::seconds elapsedTime, totalTime; std::string stateIcon = ""; bool no_song = song_.get() == nullptr; - if (stopped() || no_song) { - if (no_song) spdlog::warn("Bug in mpd: no current song but state is not stopped."); + bool is_stopped = stopped(); + if (is_stopped || no_song) { + if (no_song && !is_stopped) spdlog::warn("mpd: no current song while state is not stopped"); format = config_["format-stopped"].isString() ? config_["format-stopped"].asString() : "stopped"; label_.get_style_context()->add_class("stopped"); @@ -145,10 +132,6 @@ void waybar::modules::MPD::setLabel() { 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)); filename = sanitize_string(getFilename()); uri = mpd_song_get_uri(song_.get()); @@ -170,17 +153,12 @@ void waybar::modules::MPD::setLabel() { std::string repeatIcon = getOptionIcon("repeat", repeatActivated); bool singleActivated = mpd_status_get_single(status_.get()); 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 { auto text = fmt::format( - fmt::runtime(format), fmt::arg("artist", artist.raw()), - fmt::arg("albumArtist", album_artist.raw()), fmt::arg("album", album.raw()), - fmt::arg("title", title.raw()), fmt::arg("date", date), fmt::arg("volume", volume), + fmt::runtime(format), fmt::arg("artist", getArtistStr(true)), + fmt::arg("albumArtist", getAlbumArtistStr(true)), fmt::arg("album", getAlbumStr(true)), + fmt::arg("title", getTitleStr(true)), fmt::arg("date", date), fmt::arg("volume", volume), fmt::arg("elapsedTime", elapsedTime), fmt::arg("totalTime", totalTime), fmt::arg("songPosition", song_pos), fmt::arg("queueLength", queue_length), fmt::arg("stateIcon", stateIcon), fmt::arg("consumeIcon", consumeIcon), @@ -202,9 +180,9 @@ void waybar::modules::MPD::setLabel() { : "MPD (connected)"; try { auto tooltip_text = fmt::format( - fmt::runtime(tooltip_format), fmt::arg("artist", artist.raw()), - fmt::arg("albumArtist", album_artist.raw()), fmt::arg("album", album.raw()), - fmt::arg("title", title.raw()), fmt::arg("date", date), fmt::arg("volume", volume), + fmt::runtime(tooltip_format), fmt::arg("artist", getArtistStr(false)), + fmt::arg("albumArtist", getAlbumArtistStr(false)), fmt::arg("album", getAlbumStr(false)), + fmt::arg("title", getTitleStr(false)), fmt::arg("date", date), fmt::arg("volume", volume), fmt::arg("elapsedTime", elapsedTime), fmt::arg("totalTime", totalTime), fmt::arg("songPosition", song_pos), fmt::arg("queueLength", queue_length), fmt::arg("stateIcon", stateIcon), fmt::arg("consumeIcon", consumeIcon), @@ -257,6 +235,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) { if (ec.category() == std::system_category()) { switch (ec.value()) { diff --git a/src/modules/mpd/state.cpp b/src/modules/mpd/state.cpp index 3d7c8561..eb95665f 100644 --- a/src/modules/mpd/state.cpp +++ b/src/modules/mpd/state.cpp @@ -152,7 +152,6 @@ bool Playing::on_timer() { return false; } - ctx_->queryMPD(); ctx_->emit(); } catch (std::exception const& e) { spdlog::warn("mpd: Playing: error: {}", e.what()); diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index e0a27c92..13ba836d 100644 --- a/src/modules/mpris/mpris.cpp +++ b/src/modules/mpris/mpris.cpp @@ -7,6 +7,7 @@ #include #include "util/scope_guard.hpp" +#include "util/utf8_string.hpp" extern "C" { #include @@ -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 // Modifies str in-place (unless width = std::string::npos) // 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(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 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; } auto Mpris::getAlbumStr(const PlayerInfo& info, bool truncated) -> 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; } auto Mpris::getTitleStr(const PlayerInfo& info, bool truncated) -> 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; } @@ -292,9 +246,9 @@ auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) -> // keep position format same as length format auto position = getPositionStr(info, truncated && truncate_hours_ && length.length() < 6); - size_t artistLen = utf8_width(artist); - size_t albumLen = utf8_width(album); - size_t titleLen = utf8_width(title); + size_t artistLen = waybar::util::utf8_width(artist); + size_t albumLen = waybar::util::utf8_width(album); + size_t titleLen = waybar::util::utf8_width(title); size_t lengthLen = length.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 // it will be, we add a "virtual separatorLen" to the dynamicLen, since we are adding the // 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; if (showArtist) artistLen += separatorLen; if (showAlbum) albumLen += separatorLen; @@ -775,9 +729,12 @@ auto Mpris::update() -> void { auto tooltip_text = fmt::format( fmt::runtime(tooltipstr), fmt::arg("player", info.name), fmt::arg("status", info.status_string), - fmt::arg("artist", getArtistStr(info, tooltip_len_limits_)), - fmt::arg("title", getTitleStr(info, tooltip_len_limits_)), - fmt::arg("album", getAlbumStr(info, tooltip_len_limits_)), + fmt::arg("artist", + std::string(Glib::Markup::escape_text(getArtistStr(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("dynamic", getDynamicStr(info, tooltip_len_limits_, false)), fmt::arg("player_icon", getIconFromJson(config_["player-icons"], info.name)), diff --git a/src/modules/niri/workspaces.cpp b/src/modules/niri/workspaces.cpp index 97d15215..a817ab9c 100644 --- a/src/modules/niri/workspaces.cpp +++ b/src/modules/niri/workspaces.cpp @@ -103,7 +103,9 @@ void Workspaces::doUpdate() { fmt::arg("output", ws["output"].asString())); } if (!config_["disable-markup"].asBool()) { - static_cast(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 { button.set_label(name); } diff --git a/src/modules/privacy/privacy.cpp b/src/modules/privacy/privacy.cpp index 904c8fd9..4e613971 100644 --- a/src/modules/privacy/privacy.cpp +++ b/src/modules/privacy/privacy.cpp @@ -71,6 +71,7 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, Gtk::Orientat auto* item = Gtk::make_managed(module, nodeType, nodePtr, orientation, pos, iconSize, transition_duration); box_.add(*item); + modules_.push_back(item); } } @@ -148,9 +149,7 @@ auto Privacy::update() -> void { bool useAudioOut = false; mutex_.lock(); - for (Gtk::Widget* widget : box_.get_children()) { - auto* module = dynamic_cast(widget); - if (module == nullptr) continue; + for (PrivacyItem* module : modules_) { switch (module->privacy_type) { case util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT: setScreenshare = true; diff --git a/src/modules/sni/host.cpp b/src/modules/sni/host.cpp index 18eac643..567fbf9f 100644 --- a/src/modules/sni/host.cpp +++ b/src/modules/sni/host.cpp @@ -178,9 +178,11 @@ void Host::addRegisteredItem(const std::string& service) { return bus_name == item->bus_name && object_path == item->object_path; }); if (it == items_.end()) { - items_.emplace_back(new Item( - bus_name, object_path, config_, bar_, [this](Item& item) { itemReady(item); }, - [this](Item& item) { itemInvalidated(item); }, on_update_)); + items_.emplace_back(std::make_unique( + bus_name, object_path, config_, bar_, + [this](Item& item) { itemReady(item); }, + [this](Item& item) { itemInvalidated(item); }, + on_update_)); } } diff --git a/src/modules/sni/item.cpp b/src/modules/sni/item.cpp index 2f368083..8e2e2570 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -190,9 +190,17 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) { } else if (name == "Status") { setStatus(get_variant(value)); } else if (name == "IconName") { - icon_name = get_variant(value); + if (has_custom_icon_) { + spdlog::trace("Item '{}': ignoring IconName update, custom icon is set", id); + } else { + icon_name = get_variant(value); + } } 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") { overlay_icon_name = get_variant(value); } else if (name == "OverlayIconPixmap") { @@ -270,11 +278,13 @@ void Item::setCustomIcon(const std::string& id) { Glib::RefPtr custom_pixbuf = Gdk::Pixbuf::create_from_file(custom_icon); icon_name = ""; // icon_name has priority over pixmap icon_pixmap = custom_pixbuf; + has_custom_icon_ = true; } catch (const Glib::Error& e) { spdlog::error("Failed to load custom icon {}: {}", custom_icon, e.what()); } } else { // if file doesn't exist it's most likely an icon_name icon_name = custom_icon; + has_custom_icon_ = true; } } } diff --git a/src/modules/sni/tray.cpp b/src/modules/sni/tray.cpp index 114aba78..19de5174 100644 --- a/src/modules/sni/tray.cpp +++ b/src/modules/sni/tray.cpp @@ -24,9 +24,6 @@ Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config) if (config_["spacing"].isUInt()) { box_.set_spacing(config_["spacing"].asUInt()); } - if (config["show-passive-items"].isBool()) { - show_passive_ = config["show-passive-items"].asBool(); - } nb_hosts_ += 1; if (config_["icons"].isObject()) { IconManager::instance().setIconsConfig(config_["icons"]); @@ -42,6 +39,8 @@ void Tray::onAdd(std::unique_ptr& item) { } else { 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(); } @@ -51,17 +50,9 @@ void Tray::onRemove(std::unique_ptr& item) { } auto Tray::update() -> void { - // Show tray only when items are available std::vector children = box_.get_children(); - if (show_passive_) { - event_box_.set_visible(!children.empty()); - } 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 + event_box_.set_visible(std::any_of(children.begin(), children.end(), + [](Gtk::Widget* child) { return child->get_visible(); })); AModule::update(); } diff --git a/src/modules/sway/language.cpp b/src/modules/sway/language.cpp index 2e1103d3..28f1ef24 100644 --- a/src/modules/sway/language.cpp +++ b/src/modules/sway/language.cpp @@ -54,21 +54,28 @@ void Language::onCmd(const struct Ipc::ipc_response& res) { std::lock_guard lock(mutex_); auto payload = parser_.parse(res.payload); std::vector used_layouts; - // Display current layout of a device with a maximum count of layouts, expecting that all will - // be OK + // Union layout names across every keyboard input so hot-plugged devices contribute their + // 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; for (Json::ArrayIndex i = 0; i < payload.size(); i++) { - auto size = payload[i][XKB_LAYOUT_NAMES_KEY].size(); - if (size > max) { - max = size; + if (payload[i]["type"].asString() != "keyboard") continue; + const auto& names = payload[i][XKB_LAYOUT_NAMES_KEY]; + if (names.size() > max) { + max = names.size(); 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]) { - used_layouts.push_back(layout.asString()); - } - + // Rebuild from scratch so init_layouts_map's duplicate-suffix pass doesn't compound across + // refreshes (e.g. "us" -> "us1" -> "us11"). + layouts_map_.clear(); init_layouts_map(used_layouts); set_current_layout(payload[max_id][XKB_ACTIVE_LAYOUT_NAME_KEY].asString()); dp.emit(); @@ -82,13 +89,26 @@ void Language::onEvent(const struct Ipc::ipc_response& res) { return; } + bool refresh_inputs = false; try { - std::lock_guard lock(mutex_); - auto payload = parser_.parse(res.payload)["input"]; - if (payload["type"].asString() == "keyboard") { - set_current_layout(payload[XKB_ACTIVE_LAYOUT_NAME_KEY].asString()); + { + std::lock_guard lock(mutex_); + auto root = parser_.parse(res.payload); + 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) { spdlog::error("Language: {}", e.what()); } @@ -125,8 +145,15 @@ auto Language::update() -> 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); - layout_ = layouts_map_[current_layout]; + layout_ = it->second; label_.get_style_context()->add_class(layout_.short_name); } diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index 6abe5383..82660fc6 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -229,7 +229,10 @@ bool Workspaces::filterButtons() { auto ws = std::find_if(workspaces_.begin(), workspaces_.end(), [it](const auto& node) { return node["name"].asString() == it->first; }); 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); needReorder = true; } else { @@ -369,11 +372,14 @@ Gtk::Button& Workspaces::addButton(const Json::Value& node) { node["name"].asString(), node["target_output"].asString(), "--no-auto-back-and-forth", node["name"].asString())); } else { - ipc_.sendCmd(IPC_COMMAND, fmt::format("workspace {} \"{}\"", - config_["disable-auto-back-and-forth"].asBool() - ? "--no-auto-back-and-forth" - : "", - node["name"].asString())); + std::string flag = config_["disable-auto-back-and-forth"].asBool() + ? "--no-auto-back-and-forth" + : ""; + if (node["num"].asInt() >= 0) { + 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) { spdlog::error("Workspaces: {}", e.what()); diff --git a/src/modules/systemd_failed_units.cpp b/src/modules/systemd_failed_units.cpp index 8d7e866f..829a7776 100644 --- a/src/modules/systemd_failed_units.cpp +++ b/src/modules/systemd_failed_units.cpp @@ -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(); /* Always update for the first time. */ dp.emit(); @@ -103,7 +106,7 @@ auto SystemdFailedUnits::notify_cb(const Glib::ustring& sender_name, void SystemdFailedUnits::RequestSystemState() { auto load = [](const char* kind, Glib::RefPtr& proxy) -> std::string { try { - if (!proxy) return "unknown"; + if (!proxy) return "ignored"; auto parameters = Glib::VariantContainerBase( g_variant_new("(ss)", "org.freedesktop.systemd1.Manager", "SystemState")); Glib::VariantContainerBase data = proxy->call_sync("Get", parameters); @@ -122,7 +125,9 @@ void SystemdFailedUnits::RequestSystemState() { system_state_ = load("systemwide", system_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"; else overall_state_ = "degraded"; diff --git a/src/modules/temperature.cpp b/src/modules/temperature.cpp index bb5ff02c..dcfb0b2f 100644 --- a/src/modules/temperature.cpp +++ b/src/modules/temperature.cpp @@ -155,3 +155,7 @@ bool waybar::modules::Temperature::isCritical(uint16_t temperature_c) { return config_["critical-threshold"].isInt() && temperature_c >= config_["critical-threshold"].asInt(); } + +void waybar::modules::Temperature::suspend() { thread_.pause(); } + +void waybar::modules::Temperature::resume() { thread_.resume(); } diff --git a/src/modules/wayfire/backend.cpp b/src/modules/wayfire/backend.cpp index 42976d20..037a61e1 100644 --- a/src/modules/wayfire/backend.cpp +++ b/src/modules/wayfire/backend.cpp @@ -347,10 +347,13 @@ auto IPC::update_state_handler(const std::string& event, const Json::Value& data if (event == "output-wset-changed") { // 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()); - auto wset_idx = data["new-wset-data"]["index"].asUInt(); - state.wsets.at(wset_idx).output = output; - output.wset_idx = wset_idx; + try { + auto& output = state.outputs.at(data["output-data"]["name"].asString()); + auto wset_idx = data["new-wset-data"]["index"].asUInt(); + state.wsets.at(wset_idx).output = output; + output.wset_idx = wset_idx; + } catch (const std::exception&) { + } return; } diff --git a/src/util/audio_backend.cpp b/src/util/audio_backend.cpp index 4087e096..fb368ca5 100644 --- a/src/util/audio_backend.cpp +++ b/src/util/audio_backend.cpp @@ -99,15 +99,17 @@ void AudioBackend::contextStateCb(pa_context* c, void* data) { nullptr, nullptr); break; case PA_CONTEXT_FAILED: - // When pulseaudio server restarts, the connection is "failed". Try to reconnect. - // pa_threaded_mainloop_lock is already acquired in callback threads. - // So there is no need to lock it again. - if (backend->context_ != nullptr) { - pa_context_disconnect(backend->context_); - pa_context_unref(backend->context_); - backend->context_ = nullptr; + if (pa_context_errno(c) != PA_ERR_CONNECTIONREFUSED) { + // When pulseaudio server restarts, the connection is "failed". Try to reconnect. + // pa_threaded_mainloop_lock is already acquired in callback threads. + // So there is no need to lock it again. + if (backend->context_ != nullptr) { + pa_context_disconnect(backend->context_); + pa_context_unref(backend->context_); + backend->context_ = nullptr; + } + backend->connectContext(); } - backend->connectContext(); break; case PA_CONTEXT_CONNECTING: case PA_CONTEXT_AUTHORIZING: diff --git a/src/util/utf8_string.cpp b/src/util/utf8_string.cpp new file mode 100644 index 00000000..f96f2bd1 --- /dev/null +++ b/src/util/utf8_string.cpp @@ -0,0 +1,62 @@ +#include + +#include +#include + +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(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 diff --git a/test/hyprland/backend.cpp b/test/hyprland/backend.cpp index ccc2da65..f2e10dae 100644 --- a/test/hyprland/backend.cpp +++ b/test/hyprland/backend.cpp @@ -15,6 +15,10 @@ namespace { class IPCTestHelper : public hyprland::IPC { public: static void resetSocketFolder() { socketFolder_.clear(); } + static void resetLuaProtocolDetection() { s_luaProtocolDetected_.reset(); } + static void setLuaProtocolDetected(bool value) { s_luaProtocolDetected_ = value; } + using hyprland::IPC::buildLuaDispatch; + using hyprland::IPC::isLuaProtocol; }; std::size_t countOpenFds() { @@ -133,3 +137,78 @@ TEST_CASE("getSocket1Reply failure paths do not leak fds", "[getSocket1Reply][fd REQUIRE(after_connect_failures == baseline); } #endif + +// --- Tests for new Lua IPC dispatch functions --- + +TEST_CASE("buildLuaDispatch workspace", "[buildLuaDispatch]") { + SECTION("numeric workspace") { + auto result = IPCTestHelper::buildLuaDispatch("workspace", "1"); + REQUIRE(result == "/dispatch hl.dsp.focus({ workspace = \"1\" })"); + } + SECTION("named workspace") { + auto result = IPCTestHelper::buildLuaDispatch("workspace", "name:term"); + REQUIRE(result == "/dispatch hl.dsp.focus({ workspace = \"name:term\" })"); + } + SECTION("relative workspace") { + auto result = IPCTestHelper::buildLuaDispatch("workspace", "e+1"); + REQUIRE(result == "/dispatch hl.dsp.focus({ workspace = \"e+1\" })"); + } +} + +TEST_CASE("buildLuaDispatch focusworkspaceoncurrentmonitor", "[buildLuaDispatch]") { + auto result = + IPCTestHelper::buildLuaDispatch("focusworkspaceoncurrentmonitor", "3"); + REQUIRE( + result == + "/dispatch hl.dsp.focus({ workspace = \"3\", on_current_monitor = true })"); +} + +TEST_CASE("buildLuaDispatch togglespecialworkspace", "[buildLuaDispatch]") { + SECTION("with name") { + auto result = + IPCTestHelper::buildLuaDispatch("togglespecialworkspace", "scratchpad"); + REQUIRE(result == + "/dispatch hl.dsp.workspace.toggle_special(\"scratchpad\")"); + } + SECTION("empty arg") { + auto result = + IPCTestHelper::buildLuaDispatch("togglespecialworkspace", ""); + REQUIRE(result == "/dispatch hl.dsp.workspace.toggle_special()"); + } +} + +TEST_CASE("buildLuaDispatch unknown dispatcher fallback", "[buildLuaDispatch]") { + auto result = + IPCTestHelper::buildLuaDispatch("unknown_dispatcher", "some_arg"); + REQUIRE(result == + "/dispatch hl.dsp.unknown_dispatcher(\"some_arg\")"); +} + +TEST_CASE("dispatch throws when Hyprland is not running", "[dispatch]") { + unsetenv("HYPRLAND_INSTANCE_SIGNATURE"); + IPCTestHelper::resetSocketFolder(); + IPCTestHelper::resetLuaProtocolDetection(); + + CHECK_THROWS(hyprland::IPC::dispatch("workspace", "1")); +} + +TEST_CASE("isLuaProtocol uses cached value and avoids socket call", + "[isLuaProtocol]") { + unsetenv("HYPRLAND_INSTANCE_SIGNATURE"); + IPCTestHelper::resetSocketFolder(); + + SECTION("cached false") { + IPCTestHelper::setLuaProtocolDetected(false); + // Should return false without throwing (no socket call needed) + REQUIRE(IPCTestHelper::isLuaProtocol() == false); + } + + SECTION("cached true") { + IPCTestHelper::setLuaProtocolDetected(true); + // Should return true without throwing (no socket call needed) + REQUIRE(IPCTestHelper::isLuaProtocol() == true); + } + + // Cleanup: reset detection so other tests aren't affected + IPCTestHelper::resetLuaProtocolDetection(); +}