Skip to content

Conversation

@cookiekop
Copy link

@cookiekop cookiekop commented Oct 22, 2025

Per #3745

Screenshot from 2025-10-22 16-03-59

@yuukibarns
Copy link

Related #4164

@cookiekop
Copy link
Author

cookiekop commented Oct 26, 2025

Related #4164

Looked at it briefly. Seems like an over-complication.

@yuukibarns
Copy link

yuukibarns commented Oct 26, 2025

I am not a coder so sorry for any technical error, but I found two problems in
this PR:

  1. The registration for IPC seems to overlook two events

    gIPC->registerForIPC("WorkspacesChanged", this);
    gIPC->registerForIPC("WorkspaceActivated", this);
    gIPC->registerForIPC("WorkspaceActiveWindowChanged", this);
    gIPC->registerForIPC("WorkspaceUrgencyChanged", this);
    gIPC->registerForIPC("WindowFocusChanged", this);
    gIPC->registerForIPC("WindowOpenedOrChanged", this);
    // The overlooked two events
    gIPC->registerForIPC("WindowClosed", this);
    gIPC->registerForIPC("WindowLayoutsChanged", this);

    Reference:
    niri_ipc.

    The event WindowClosed is used to cleanup the window icon when a window is
    closed, otherwise the window will remain in the taskbar until other events
    trigger update.

    I haven't figured out what the event WindowLayoutsChanged do under the
    hood, but the problem I want to address is that when I manually change the
    window order in one workspace, the icon order in the taskbar won't change.

  2. pos_in_scrolling_layout can be null if the window is floating.

    std::sort(sorted_windows_data.begin(), sorted_windows_data.end(),
            [](const Json::Value& a, const Json::Value& b) {
              auto layoutA = a["layout"];
              auto layoutB = b["layout"];
              return layoutA["pos_in_scrolling_layout"][0].asInt() <
                     layoutB["pos_in_scrolling_layout"][0].asInt();
            });

    Suggested change by DeepSeek (sorry for my technical incompetence):

    std::sort(sorted_windows_data.begin(), sorted_windows_data.end(),
              [](const Json::Value& a, const Json::Value& b) {
                auto layoutA = a["layout"];
                auto layoutB = b["layout"];
                
                // Handle null positions (floating windows)
                if (layoutA["pos_in_scrolling_layout"].isNull()) {
                    return false; // Floating windows go to the end
                }
                if (layoutB["pos_in_scrolling_layout"].isNull()) {
                    return true; // Tiled windows before floating
                }
                
                // Both are tiled windows - sort by position
                return layoutA["pos_in_scrolling_layout"][0].asInt() <
                       layoutB["pos_in_scrolling_layout"][0].asInt();
              });

the problem I want to address is that when I manually change the
window order in one workspace, the icon order in the taskbar won't change.

I haven't solved this problem yet.

@yuukibarns
Copy link

yuukibarns commented Oct 26, 2025

the problem I want to address is that when I manually change the window order
in one workspace, the icon order in the taskbar won't change.

I have solved the problem.

#4530, this change:

image

In brief, the implementation in backend.cpp is too old to handle the event
WindowLayoutsChanged introduced in YaLTeR/niri#1265 which expose the
position of windows in scrolling layout that we used to sort windows.

After merge the above commit and my suggested changes in registration of IPC,
both closing and swapping windows now results in immediate change in the
taskbar.

Note

I don't know if the null check is necessary since I am not familiar with C++
and the null checks underneath.

@cookiekop
Copy link
Author

cookiekop commented Oct 26, 2025

Glad you figured it out. I am using pretty recent niri build(instead of the release version). Maybe that is the reason I did not notice the issue.

pos_in_scrolling_layout can be null if the window is floating.

As for this one, I think you are right to be thorough. But I check the source code of JsonCPP here. nullvalue will be returned as 0. So it should be fine~

@yuukibarns
Copy link

But there is another problem now, can you try to track the memory usage of waybar while switching focus between windows many times? There seems to be a memory leak in the updateTaskbar function!

@yuukibarns
Copy link

I have solved it again, :P.

void PrivacyItem::update_tooltip() {
// Removes all old nodes
for (auto *child : tooltip_window.get_children()) {
tooltip_window.remove(*child);
// despite the remove, still needs a delete to prevent memory leak. Speculating that this might
// work differently in GTK4.
delete child;
}

#4092, this PR solved the similar memory leak in privacy module.

From gtk3 documentation:

If you don’t want to use widget again it’s usually more efficient to simply destroy it directly using gtk_widget_destroy() since this will remove it from the container and help break any circular reference count cycles.

It seems to be some gtk3 quirks.

So the solution is simply delete the child...

@cookiekop
Copy link
Author

Thanks! Pushed with the fix.

@yuukibarns
Copy link

By the way the implementation in hyprland workspace taskbar which we use as the reference seems to have the same memory leak problem but nobody has come up with an issue yet, so I am not sure what under the earth the memory management of gtk3 does...

@cookiekop
Copy link
Author

Exactly. I actually referred to the code from hyprland here.

@yuukibarns
Copy link

What about other changes I suggested, like the overlooked events in the registration of IPC and the missing implementation in backend.cpp for the WindowLayoutsChanged?

@cookiekop
Copy link
Author

cookiekop commented Oct 26, 2025

I have not observed any problems with "closing window" or "changing layout" with the current implementation...
But I will definitely look through the PR you mentioned. If something is necessary, I will add it here~

@cookiekop
Copy link
Author

Did i make myself understood? Any update?

I think you do. But I am working on other things... You can add your changes here(or anywhere else)~ The format thing is pretty cool.

I have not observed any problems with "closing window" or "changing layout" with the current implementation...

❓ , but in the current implementation the taskbar will not update immediately after a window has closed since it does not listen to the WindowClosed event. And without the missing implementation in backend.cpp, change the window order in one workspace won't change the icon order in the taskbar, icons simply line up with the order they're created.

For me, they are working fine. I'm not sure about the reasons.

@yuukibarns
Copy link

But I am working on other things... You can add your changes here(or anywhere else)~

A late thank for you and your creating this elegant and amazing PR 🎉 👍 , without your PR, I would wait for that over-complicated PR for whoever knows how long.

I am trying to complete the configuration to match up with hyprland's workspace taskbar. I find it may be difficult for the coming people who want to work on this module to add some feature since the GTK seems to be cursed and some time-consuming groping is necessary for add even a small feature.

When I am done, I will send a diff file here based on your PR, you can take any part as you like, after all the credits is yours.

@cookiekop
Copy link
Author

I was just trying to add things that I like. So I might be over-simplifying things sometimes. Glad I can help~😁

@yuukibarns
Copy link

yuukibarns commented Oct 27, 2025

diff --git a/include/modules/niri/workspaces.hpp b/include/modules/niri/workspaces.hpp
index 328586c3..19414177 100644
--- a/include/modules/niri/workspaces.hpp
+++ b/include/modules/niri/workspaces.hpp
@@ -23,7 +23,7 @@ class Workspace {
 
  private:
   std::string getIcon(const std::string& value, const Json::Value& ws);
-  void updateTaskbar(const std::vector<Json::Value>& windows_data);
+  void updateTaskbar(const std::vector<Json::Value>& windows_data, const uint64_t active_window_id);
 
   IconLoader iconLoader_;
   uint64_t id_;
diff --git a/man/waybar-niri-workspaces.5.scd b/man/waybar-niri-workspaces.5.scd
index 230181c8..f2dec6d6 100644
--- a/man/waybar-niri-workspaces.5.scd
+++ b/man/waybar-niri-workspaces.5.scd
@@ -67,7 +67,17 @@ Addressed by *niri/workspaces*
 		*separator*: ++
 	typeof: string ++
 	default: " " ++
-	The separator to be used between windows in a workspace. ++
+	The separator to be used between windows in a workspace.
+
+		*format*: ++
+	typeof: string ++
+	default: "{icon}" ++
+	Format to use for each window in the workspace taskbar. Available placeholders are {icon}, {title} and {app_id}.
+
+		*tooltip-format*: ++
+	typeof: string ++
+	default: "{title}" ++
+	The format, how information in the tooltip should be displayed. Available placeholders are {title} and {app_id}. ++
 	This setting is ignored if *workspace-taskbar.enable* is set to true.
 
 # FORMAT REPLACEMENTS
@@ -127,3 +137,5 @@ Additional to workspace name matching, the following *format-icons* can be set.
 - *#workspaces .taskbar-window* (each window in the taskbar, only if 'workspace-taskbar.enable' is true)
 - *#workspaces .taskbar-window.focused* (applied to the focused window)
 - *#workspaces .taskbar-window.urgent* (applied to urgent windows)
+- *#workspaces .taskbar-window.floating* (applied to floating windows)
+- *#workspaces .taskbar-window.active* (applied to the active window within every workspace)
diff --git a/src/modules/niri/backend.cpp b/src/modules/niri/backend.cpp
index fa4dc287..528b7b5c 100644
--- a/src/modules/niri/backend.cpp
+++ b/src/modules/niri/backend.cpp
@@ -202,6 +202,18 @@ void IPC::parseIPC(const std::string &line) {
       for (auto &win : windows_) {
         win["is_focused"] = focused && win["id"].asUInt64() == id;
       }
+    } else if (const auto &payload = ev["WindowLayoutsChanged"]) {
+      const auto &values = payload["changes"];
+      for (const auto &changed : values) {
+        const auto id = changed[0].asUInt64();
+        const auto &change = changed[1];
+        for (auto &win : windows_) {
+          if (win["id"].asUInt64() == id) {
+            win["layout"] = change;
+            break;
+          }
+        }
+      }
     }
   }
 
diff --git a/src/modules/niri/workspaces.cpp b/src/modules/niri/workspaces.cpp
index 6f1a0150..4e8f7ba0 100644
--- a/src/modules/niri/workspaces.cpp
+++ b/src/modules/niri/workspaces.cpp
@@ -71,23 +71,53 @@ std::string Workspace::getIcon(const std::string& value, const Json::Value& ws)
   return value;
 }
 
-void Workspace::updateTaskbar(const std::vector<Json::Value>& windows_data) {
+void Workspace::updateTaskbar(const std::vector<Json::Value>& windows_data,
+                              const uint64_t active_window_id) {
   if (!taskBarConfig_.get("enable", false).asBool()) return;
 
   for (auto child : content_.get_children()) {
     if (child != &label_) {
       content_.remove(*child);
+      // despite the remove, still needs a delete to prevent memory leak. Speculating that this
+      // might work differently in GTK4.
       delete child;
     }
   }
 
   auto separator = taskBarConfig_.get("separator", " ").asString();
 
+  auto format = taskBarConfig_.get("format", "{icon}").asString();
+  bool taskbarWithIcon = false;
+  std::string taskbarFormatBefore, taskbarFormatAfter;
+
+  if (format != "") {
+    auto parts = split(format, "{icon}", 1);
+    taskbarFormatBefore = parts[0];
+    if (parts.size() > 1) {
+      taskbarWithIcon = true;
+      taskbarFormatAfter = parts[1];
+    }
+  } else {
+    taskbarWithIcon = true;  // default to icon-only
+  }
+
+  auto format_tooltip = taskBarConfig_.get("tooltip-format", "{title}").asString();
+
   auto sorted_windows_data = windows_data;
   std::sort(sorted_windows_data.begin(), sorted_windows_data.end(),
             [](const Json::Value& a, const Json::Value& b) {
               auto layoutA = a["layout"];
               auto layoutB = b["layout"];
+
+              // Handle null positions (floating windows)
+              if (layoutA["pos_in_scrolling_layout"].isNull()) {
+                return false;  // Floating windows go to the end
+              }
+              if (layoutB["pos_in_scrolling_layout"].isNull()) {
+                return true;  // Tiled windows before floating
+              }
+
+              // Both are tiled windows - sort by position
               return layoutA["pos_in_scrolling_layout"][0].asInt() <
                      layoutB["pos_in_scrolling_layout"][0].asInt();
             });
@@ -100,10 +130,18 @@ void Workspace::updateTaskbar(const std::vector<Json::Value>& windows_data) {
     }
 
     auto window_box = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL);
-    window_box->set_tooltip_text(window["title"].asString());
+    if (!format_tooltip.empty()) {
+      auto txt =
+          fmt::format(fmt::runtime(format_tooltip), fmt::arg("title", window["title"].asString()),
+                      fmt::arg("app_id", window["app_id"].asString()));
+      window_box->set_tooltip_text(txt);
+    }
     window_box->get_style_context()->add_class("taskbar-window");
     if (window["is_focused"].asBool()) window_box->get_style_context()->add_class("focused");
+    if (window["is_floating"].asBool()) window_box->get_style_context()->add_class("floating");
     if (window["is_urgent"].asBool()) window_box->get_style_context()->add_class("urgent");
+    if (window["id"].asUInt64() == active_window_id)
+      window_box->get_style_context()->add_class("active");
     auto event_box = Gtk::make_managed<Gtk::EventBox>();
     event_box->add(*window_box);
     if (!config_["disable-click"].asBool()) {
@@ -128,11 +166,31 @@ void Workspace::updateTaskbar(const std::vector<Json::Value>& windows_data) {
       event_box->signal_button_press_event().connect(
           sigc::bind(sigc::ptr_fun(func_ptr), window["id"].asUInt64()));
     }
-    auto window_icon = Gtk::make_managed<Gtk::Image>();
-    iconLoader_.image_load_icon(
-        *window_icon, IconLoader::get_app_info_from_app_id_list(window["app_id"].asString()),
-        taskBarConfig_.get("icon-size", 16).asInt());
-    window_box->pack_start(*window_icon, false, false);
+
+    auto text_before = fmt::format(fmt::runtime(taskbarFormatBefore),
+                                   fmt::arg("title", window["title"].asString()),
+                                   fmt::arg("app_id", window["app_id"].asString()));
+    if (!text_before.empty()) {
+      auto window_label_before = Gtk::make_managed<Gtk::Label>(text_before);
+      window_box->pack_start(*window_label_before, true, true);
+    }
+
+    if (taskbarWithIcon) {
+      auto window_icon = Gtk::make_managed<Gtk::Image>();
+      iconLoader_.image_load_icon(
+          *window_icon, IconLoader::get_app_info_from_app_id_list(window["app_id"].asString()),
+          taskBarConfig_.get("icon-size", 16).asInt());
+      window_box->pack_start(*window_icon, false, false);
+    }
+
+    auto text_after =
+        fmt::format(fmt::runtime(taskbarFormatAfter), fmt::arg("title", window["title"].asString()),
+                    fmt::arg("app_id", window["app_id"].asString()));
+    if (!text_after.empty()) {
+      auto window_label_after = Gtk::make_managed<Gtk::Label>(text_after);
+      window_box->pack_start(*window_label_after, true, true);
+    }
+
     content_.pack_start(*event_box, false, false);
   }
 }
@@ -183,7 +241,7 @@ void Workspace::update(const Json::Value& workspace_data,
   else
     label_.set_text(name);
 
-  updateTaskbar(windows_data);
+  updateTaskbar(windows_data, workspace_data["active_window_id"].asUInt64());
 
   if (config_["current-only"].asBool()) {
     const auto* property = config_["all-outputs"].asBool() ? "is_focused" : "is_active";
@@ -212,6 +270,8 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value&
   gIPC->registerForIPC("WorkspaceUrgencyChanged", this);
   gIPC->registerForIPC("WindowFocusChanged", this);
   gIPC->registerForIPC("WindowOpenedOrChanged", this);
+  gIPC->registerForIPC("WindowClosed", this);
+  gIPC->registerForIPC("WindowLayoutsChanged", this);
 
   dp.emit();
 }

The change of updateTaskbar's signature is used to add the CSS class active
, which is applied to the active window within every workspace.

I add two configuration fields, format and tooltip-format. format decides
how to display the window title etc after or behind the icon and
tooltip-format decides how the information in the tooltip will be displayed. I
am not familiar with C++, these parts of code are taken from
hyprland/workspaces and wlr/taskbar.

All other changes I have explained in the comments above.

Let me explain my reason behind adding the CSS class active.

What makes Niri different from other compositors, in my humble opinion, is that
we can move around in two dimensions instead of the usual one dimension as in
Hyprland. However, just like a mechanical system with two degrees of freedom, we
need two variables to describe window positions even if we've flattened them
into the one dimensional Waybar. What are these two variables? Yes, it is the
two CSS classes I add in this diff.

Imagine the situation when you move up and down between workspace, can you
predict which window is active within each workspace, I can't, and after moving
from one workspace to another I always hesitate for a while, wondering which
window I am focusing on.

In fact, in niri, there are two independent variables to describe the position
of windows: the active workspace and the active window within each workspace,
described by the class #workspaces button.active and our newly added class
#workspaces .taskbar-window.active. Moving between workspaces won't change the
active window within each workspace, and moving within one workspace of course
won't change the active workspace.

Warning

I only have one laptop and have never tried multi-monitor PC, so I don't know
if my method works in multi-monitor, i.e., multiple-active-workspace
situation.

In fact there is the third dimension, the floating and the scrolling layouts. So
I additionally add a floating class (just an one-line change) for floating
windows.

screenrecord.mp4
"niri/workspaces": {
  "format": "{index} ",
  "workspace-taskbar": {
    "enable": true,
    "icon-size": 16,
    "separator": "",
    "format": "{icon}  {title:10.10}",
    "tooltip-format": "{title} | {app_id}"
  }
},
#workspaces {
  color: transparent;
  margin-right: 2px;
  margin-left: 2px;
}
#workspaces button {
  padding: 0;
  margin-right: 2px;
  margin-left: 2px;
  min-width: 0;
  color: @content_inactive;
  opacity: 0.4;
}
#workspaces button.active {
  color: @content_main;
  opacity: 0.8;
}
#workspaces button.focused {
  color: @content_main;
  opacity: 1.0;
}
#workspaces button.urgent {
  background: rgba(255, 200, 0, 0.35);
  color: @warning_color;
  opacity: 1.0;
}
#workspaces button:hover {
  background: @bg_hover;
  color: @content_main;
}

#workspaces .workspace-label {
  font-size: 12px;
  padding-left: 3px;
  margin-right: 4px;
  margin-top: 4px;
  margin-bottom: 4px;
  border-radius: 5px;
  border-width: 1px;
  border-style: solid;
  border-color: @border_floating;
}
#workspaces .taskbar-window {
  padding-top: 1px;
  padding-bottom: 1px;
  padding-left: 3px;
  padding-right: 3px;
  color: @content_main;
}
#workspaces .taskbar-window.floating {
  border-left: 1px solid @border_floating;
}
#workspaces .taskbar-window.active {
  border-bottom: 2px solid @content_main;
}
#workspaces .taskbar-window.focused {
  background: @bg_active;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants