diff --git a/include/factory.hpp b/include/factory.hpp index a853841c..55031b83 100644 --- a/include/factory.hpp +++ b/include/factory.hpp @@ -9,6 +9,7 @@ #ifdef HAVE_SWAY #include "modules/sway/language.hpp" #include "modules/sway/mode.hpp" +#include "modules/sway/scratchpad.hpp" #include "modules/sway/window.hpp" #include "modules/sway/workspaces.hpp" #endif @@ -74,6 +75,7 @@ #include "bar.hpp" #include "modules/custom.hpp" #include "modules/temperature.hpp" +#include "modules/user.hpp" namespace waybar { diff --git a/include/modules/sway/scratchpad.hpp b/include/modules/sway/scratchpad.hpp new file mode 100644 index 00000000..e68e7726 --- /dev/null +++ b/include/modules/sway/scratchpad.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +#include +#include + +#include "ALabel.hpp" +#include "bar.hpp" +#include "client.hpp" +#include "modules/sway/ipc/client.hpp" +#include "util/json.hpp" + +namespace waybar::modules::sway { +class Scratchpad : public ALabel { + public: + Scratchpad(const std::string&, const Json::Value&); + ~Scratchpad() = default; + auto update() -> void; + + private: + auto getTree() -> void; + auto onCmd(const struct Ipc::ipc_response&) -> void; + auto onEvent(const struct Ipc::ipc_response&) -> void; + + std::string tooltip_format_; + bool show_empty_; + bool tooltip_enabled_; + std::string tooltip_text_; + int count_; + std::mutex mutex_; + Ipc ipc_; + util::JsonParser parser_; +}; +} // namespace waybar::modules::sway \ No newline at end of file diff --git a/include/modules/user.hpp b/include/modules/user.hpp new file mode 100644 index 00000000..41e7884e --- /dev/null +++ b/include/modules/user.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include + +#include "AIconLabel.hpp" +#include "util/sleeper_thread.hpp" + +namespace waybar::modules { +class User : public AIconLabel { + public: + User(const std::string&, const Json::Value&); + ~User() = default; + auto update() -> void; + + private: + util::SleeperThread thread_; + + Glib::RefPtr pixbuf_; + + static constexpr inline int defaultUserImageWidth_ = 20; + static constexpr inline int defaultUserImageHeight_ = 20; + + long uptime_as_seconds(); + std::string get_user_login(); + std::string get_user_home_dir(); + std::string get_default_user_avatar_path(); + void init_default_user_avatar(int width, int height); + void init_user_avatar(const std::string& path, int width, int height); + void init_avatar(const Json::Value& config); + void init_update_worker(); +}; +} // namespace waybar::modules diff --git a/man/waybar-sway-scratchpad.5.scd b/man/waybar-sway-scratchpad.5.scd new file mode 100644 index 00000000..11fc32c0 --- /dev/null +++ b/man/waybar-sway-scratchpad.5.scd @@ -0,0 +1,64 @@ +waybar-sway-scratchpad(5) + +# NAME + +waybar - sway scratchpad module + +# DESCRIPTION + +The *scratchpad* module displays the scratchpad status in Sway + +# CONFIGURATION + +Addressed by *sway/scratchpad* + +*format*: ++ + typeof: string ++ + default: {icon} {count} ++ + The format, how information should be displayed. + +*show-empty*: ++ + typeof: bool ++ + default: false ++ + Option to show module when scratchpad is empty. + +*format-icons*: ++ + typeof: array/object ++ + Based on the current scratchpad window counts, the corresponding icon gets selected. + +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to disable tooltip on hover. + +*tooltip-format*: ++ + typeof: string ++ + default: {app}: {title} ++ + The format, how information in the tooltip should be displayed. + +# FORMAT REPLACEMENTS + +*{icon}*: Icon, as defined in *format-icons*. + +*{count}*: Number of windows in the scratchpad. + +*{app}*: Name of the application in the scratchpad. + +*{title}*: Title of the application in the scratchpad. + +# EXAMPLES + +``` +"sway/scratchpad": { + "format": "{icon} {count}", + "show-empty": false, + "format-icons": ["", ""], + "tooltip": true, + "tooltip-format": "{app}: {title}" +} +``` + +# STYLE + +- *#scratchpad* +- *#scratchpad.empty* diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in index fafc2b36..54340f21 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -273,6 +273,7 @@ A module group is defined by specifying a module named "group/some-group-name". - *waybar-river-window(5)* - *waybar-states(5)* - *waybar-sway-mode(5)* +- *waybar-sway-scratchpad(5)* - *waybar-sway-window(5)* - *waybar-sway-workspaces(5)* - *waybar-wlr-taskbar(5)* diff --git a/meson.build b/meson.build index 6eccb7a5..aab5a492 100644 --- a/meson.build +++ b/meson.build @@ -150,6 +150,7 @@ src_files = files( 'src/modules/disk.cpp', 'src/modules/idle_inhibitor.cpp', 'src/modules/temperature.cpp', + 'src/modules/user.cpp', 'src/main.cpp', 'src/bar.cpp', 'src/client.cpp', @@ -187,7 +188,8 @@ src_files += [ 'src/modules/sway/mode.cpp', 'src/modules/sway/language.cpp', 'src/modules/sway/window.cpp', - 'src/modules/sway/workspaces.cpp' + 'src/modules/sway/workspaces.cpp', + 'src/modules/sway/scratchpad.cpp' ] if true @@ -374,6 +376,7 @@ if scdoc.found() 'waybar-river-window.5.scd', 'waybar-sway-language.5.scd', 'waybar-sway-mode.5.scd', + 'waybar-sway-scratchpad.5.scd', 'waybar-sway-window.5.scd', 'waybar-sway-workspaces.5.scd', 'waybar-temperature.5.scd', diff --git a/resources/config b/resources/config index 6a753acf..ad76e937 100644 --- a/resources/config +++ b/resources/config @@ -5,7 +5,7 @@ // "width": 1280, // Waybar width "spacing": 4, // Gaps between modules (4px) // Choose the order of the modules - "modules-left": ["sway/workspaces", "sway/mode", "custom/media"], + "modules-left": ["sway/workspaces", "sway/mode", "sway/scratchpad", "custom/media"], "modules-center": ["sway/window"], "modules-right": ["mpd", "idle_inhibitor", "pulseaudio", "network", "cpu", "memory", "temperature", "backlight", "keyboard-state", "sway/language", "battery", "battery#bat2", "clock", "tray"], // Modules configuration @@ -36,6 +36,13 @@ "sway/mode": { "format": "{}" }, + "sway/scratchpad": { + "format": "{icon} {count}", + "show-empty": false, + "format-icons": ["", ""], + "tooltip": true, + "tooltip-format": "{app}: {title}" + }, "mpd": { "format": "{stateIcon} {consumeIcon}{randomIcon}{repeatIcon}{singleIcon}{artist} - {album} - {title} ({elapsedTime:%M:%S}/{totalTime:%M:%S}) ⸨{songPosition}|{queueLength}⸩ {volume}% ", "format-disconnected": "Disconnected ", diff --git a/resources/style.css b/resources/style.css index bfee80de..40d870af 100644 --- a/resources/style.css +++ b/resources/style.css @@ -34,6 +34,14 @@ window#waybar.chromium { border: none; } +button { + /* Use box-shadow instead of border so the text isn't offset */ + box-shadow: inset 0 -3px transparent; + /* Avoid rounded borders under each button name */ + border: none; + border-radius: 0; +} + /* https://github.com/Alexays/Waybar/wiki/FAQ#the-workspace-buttons-have-a-strange-hover-effect */ button:hover { background: inherit; @@ -44,11 +52,6 @@ button:hover { padding: 0 5px; background-color: transparent; color: #ffffff; - /* Use box-shadow instead of border so the text isn't offset */ - box-shadow: inset 0 -3px transparent; - /* Avoid rounded borders under each workspace name */ - border: none; - border-radius: 0; } #workspaces button:hover { @@ -82,6 +85,7 @@ button:hover { #tray, #mode, #idle_inhibitor, +#scratchpad, #mpd { padding: 0 10px; color: #ffffff; @@ -256,3 +260,11 @@ label:focus { #keyboard-state > label.locked { background: rgba(0, 0, 0, 0.2); } + +#scratchpad { + background: rgba(0, 0, 0, 0.2); +} + +#scratchpad.empty { + background-color: transparent; +} diff --git a/src/factory.cpp b/src/factory.cpp index 13b7803f..9b9dde89 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -35,6 +35,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "sway/language") { return new waybar::modules::sway::Language(id, config_[name]); } + if (ref == "sway/scratchpad") { + return new waybar::modules::sway::Scratchpad(id, config_[name]); + } #endif #ifdef HAVE_WLR if (ref == "wlr/taskbar") { @@ -81,6 +84,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "clock") { return new waybar::modules::Clock(id, config_[name]); } + if (ref == "user") { + return new waybar::modules::User(id, config_[name]); + } if (ref == "disk") { return new waybar::modules::Disk(id, config_[name]); } diff --git a/src/modules/pulseaudio.cpp b/src/modules/pulseaudio.cpp index a396a9d5..95f5ee53 100644 --- a/src/modules/pulseaudio.cpp +++ b/src/modules/pulseaudio.cpp @@ -36,6 +36,7 @@ waybar::modules::Pulseaudio::Pulseaudio(const std::string &id, const Json::Value } waybar::modules::Pulseaudio::~Pulseaudio() { + pa_context_disconnect(context_); mainloop_api_->quit(mainloop_api_, 0); pa_threaded_mainloop_stop(mainloop_); pa_threaded_mainloop_free(mainloop_); diff --git a/src/modules/sway/scratchpad.cpp b/src/modules/sway/scratchpad.cpp new file mode 100644 index 00000000..59e30530 --- /dev/null +++ b/src/modules/sway/scratchpad.cpp @@ -0,0 +1,82 @@ +#include "modules/sway/scratchpad.hpp" + +#include + +#include + +namespace waybar::modules::sway { +Scratchpad::Scratchpad(const std::string& id, const Json::Value& config) + : ALabel(config, "scratchpad", id, + config["format"].isString() ? config["format"].asString() : "{icon} {count}"), + tooltip_format_(config_["tooltip-format"].isString() ? config_["tooltip-format"].asString() + : "{app}: {title}"), + show_empty_(config_["show-empty"].isBool() ? config_["show-empty"].asBool() : false), + tooltip_enabled_(config_["tooltip"].isBool() ? config_["tooltip"].asBool() : true), + tooltip_text_(""), + count_(0) { + ipc_.subscribe(R"(["window"])"); + ipc_.signal_event.connect(sigc::mem_fun(*this, &Scratchpad::onEvent)); + ipc_.signal_cmd.connect(sigc::mem_fun(*this, &Scratchpad::onCmd)); + + getTree(); + + ipc_.setWorker([this] { + try { + ipc_.handleEvent(); + } catch (const std::exception& e) { + spdlog::error("Scratchpad: {}", e.what()); + } + }); +} +auto Scratchpad::update() -> void { + if (count_ || show_empty_) { + event_box_.show(); + label_.set_markup( + fmt::format(format_, fmt::arg("icon", getIcon(count_, "", config_["format-icons"].size())), + fmt::arg("count", count_))); + if (tooltip_enabled_) { + label_.set_tooltip_markup(tooltip_text_); + } + } else { + event_box_.hide(); + } + if (count_) { + label_.get_style_context()->remove_class("empty"); + } else { + label_.get_style_context()->add_class("empty"); + } + ALabel::update(); +} + +auto Scratchpad::getTree() -> void { + try { + ipc_.sendCmd(IPC_GET_TREE); + } catch (const std::exception& e) { + spdlog::error("Scratchpad: {}", e.what()); + } +} + +auto Scratchpad::onCmd(const struct Ipc::ipc_response& res) -> void { + try { + std::lock_guard lock(mutex_); + auto tree = parser_.parse(res.payload); + count_ = tree["nodes"][0]["nodes"][0]["floating_nodes"].size(); + if (tooltip_enabled_) { + tooltip_text_.clear(); + for (const auto& window : tree["nodes"][0]["nodes"][0]["floating_nodes"]) { + tooltip_text_.append(fmt::format(tooltip_format_ + '\n', + fmt::arg("app", window["app_id"].asString()), + fmt::arg("title", window["name"].asString()))); + } + if (!tooltip_text_.empty()) { + tooltip_text_.pop_back(); + } + } + dp.emit(); + } catch (const std::exception& e) { + spdlog::error("Scratchpad: {}", e.what()); + } +} + +auto Scratchpad::onEvent(const struct Ipc::ipc_response& res) -> void { getTree(); } +} // namespace waybar::modules::sway \ No newline at end of file diff --git a/src/modules/user.cpp b/src/modules/user.cpp new file mode 100644 index 00000000..88da0a45 --- /dev/null +++ b/src/modules/user.cpp @@ -0,0 +1,114 @@ +#include "modules/user.hpp" + +#include +#include +#include + +#include +#include +#include + +#if HAVE_CPU_LINUX +#include +#endif + +#if HAVE_CPU_BSD +#include +#endif + +namespace waybar::modules { +User::User(const std::string& id, const Json::Value& config) + : AIconLabel(config, "user", id, "{user} {work_H}:{work_M}", 60, false, false, true) { + if (AIconLabel::iconEnabled()) { + this->init_avatar(AIconLabel::config_); + } + this->init_update_worker(); +} + +long User::uptime_as_seconds() { + long uptime = 0; + +#if HAVE_CPU_LINUX + struct sysinfo s_info; + if (0 == sysinfo(&s_info)) { + uptime = s_info.uptime; + } +#endif + +#if HAVE_CPU_BSD + struct timespec s_info; + if (0 == clock_gettime(CLOCK_UPTIME_PRECISE, &s_info)) { + uptime = s_info.tv_sec; + } +#endif + + return uptime; +} + +std::string User::get_user_login() { return Glib::get_user_name(); } + +std::string User::get_user_home_dir() { return Glib::get_home_dir(); } + +void User::init_update_worker() { + this->thread_ = [this] { + ALabel::dp.emit(); + auto now = std::chrono::system_clock::now(); + auto diff = now.time_since_epoch() % ALabel::interval_; + this->thread_.sleep_for(ALabel::interval_ - diff); + }; +} + +void User::init_avatar(const Json::Value& config) { + int height = + config["height"].isUInt() ? config["height"].asUInt() : this->defaultUserImageHeight_; + int width = config["width"].isUInt() ? config["width"].asUInt() : this->defaultUserImageWidth_; + + if (config["avatar"].isString()) { + std::string userAvatar = config["avatar"].asString(); + if (!userAvatar.empty()) { + this->init_user_avatar(userAvatar, width, height); + return; + } + } + + this->init_default_user_avatar(width, width); +} + +std::string User::get_default_user_avatar_path() { + return this->get_user_home_dir() + "/" + ".face"; +} + +void User::init_default_user_avatar(int width, int height) { + this->init_user_avatar(this->get_default_user_avatar_path(), width, height); +} + +void User::init_user_avatar(const std::string& path, int width, int height) { + this->pixbuf_ = Gdk::Pixbuf::create_from_file(path, width, height); + AIconLabel::image_.set(this->pixbuf_); +} + +auto User::update() -> void { + std::string systemUser = this->get_user_login(); + std::transform(systemUser.cbegin(), systemUser.cend(), systemUser.begin(), + [](unsigned char c) { return std::toupper(c); }); + + long uptimeSeconds = this->uptime_as_seconds(); + auto workSystemTimeSeconds = std::chrono::seconds(uptimeSeconds); + auto currentSystemTime = std::chrono::system_clock::now(); + auto startSystemTime = currentSystemTime - workSystemTimeSeconds; + long workSystemDays = uptimeSeconds / 86400; + + auto label = fmt::format(ALabel::format_, fmt::arg("up_H", fmt::format("{:%H}", startSystemTime)), + fmt::arg("up_M", fmt::format("{:%M}", startSystemTime)), + fmt::arg("up_d", fmt::format("{:%d}", startSystemTime)), + fmt::arg("up_m", fmt::format("{:%m}", startSystemTime)), + fmt::arg("up_Y", fmt::format("{:%Y}", startSystemTime)), + fmt::arg("work_d", workSystemDays), + fmt::arg("work_H", fmt::format("{:%H}", workSystemTimeSeconds)), + fmt::arg("work_M", fmt::format("{:%M}", workSystemTimeSeconds)), + fmt::arg("work_S", fmt::format("{:%S}", workSystemTimeSeconds)), + fmt::arg("user", systemUser)); + ALabel::label_.set_markup(label); + ALabel::update(); +} +}; // namespace waybar::modules