commit e285b3c47b2fc7a4cdaba9a427d2958410e64620
Author: Orestis Floros <orestisflo@gmail.com>
Date:   Tue Dec 23 15:11:03 2025 +0100

    Fix i3bar workspace buttons for primary screen (#6564)
    
    git bisect: c7344095ec242c5e2616841de27f11343a6010ee is the first bad
    commit
    ```
    c7344095ec242c5e2616841de27f11343a6010ee (HEAD) Fix leak sanitizer memleaks (#6520)
     i3bar/src/ipc.c         |  5 +++--
     i3bar/src/workspaces.c  | 58 +++++++++++++++++++++++++++++++++-------------------------
     i3bar/src/xcb.c         |  1 +
     include/libi3.h         | 10 ++++------
     src/config_directives.c |  1 +
     src/config_parser.c     | 85 +++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------
     src/load_layout.c       |  2 +-
     src/restore_layout.c    |  4 ++++
     8 files changed, 96 insertions(+), 70 deletions(-)
    ```
    
    To handle race conditions in the test, I tested with:
    ```diff
    diff --git a/i3bar/src/workspaces.c b/i3bar/src/workspaces.c
    index d712750d..0c481d18 100644
    --- a/i3bar/src/workspaces.c
    +++ b/i3bar/src/workspaces.c
    @@ -286,6 +286,8 @@ void parse_workspaces_json(const unsigned char *json, const size_t size) {
             }
         }
    
    +    sleep(1);
    +
         yajl_free(handle);
         FREE(params.cur_key);
     }
    ```
    
    fixes https://github.com/i3/i3/issues/6560

diff --git a/i3bar/src/workspaces.c b/i3bar/src/workspaces.c
index 26abf452..90b8c4ac 100644
--- a/i3bar/src/workspaces.c
+++ b/i3bar/src/workspaces.c
@@ -19,6 +19,7 @@ struct workspaces_json_params {
     struct ws_head *workspaces;
     i3_ws *workspaces_walk;
     char *cur_key;
+    bool need_output;
     bool parsing_rect;
 };
 
@@ -160,6 +161,7 @@ static int workspaces_string_cb(void *params_, const unsigned char *val, const s
             TAILQ_INSERT_TAIL(ws->output->workspaces, ws, tailq);
         }
 
+        params->need_output = false;
         FREE(output_name);
         FREE(params->cur_key);
 
@@ -181,6 +183,7 @@ static int workspaces_start_map_cb(void *params_) {
         new_workspace->num = -1;
 
         params->workspaces_walk = new_workspace;
+        params->need_output = true;
         params->parsing_rect = false;
     } else {
         params->parsing_rect = true;
@@ -202,6 +205,15 @@ static int workspaces_end_map_cb(void *params_) {
         return 1; /* workspace already assigned to output */
     }
 
+    /* If we processed the output field but didn't find the output, the
+     * workspace belongs to an output this bar instance doesn't manage. */
+    if (!params->need_output) {
+        I3STRING_FREE(ws->name);
+        FREE(ws->canonical_name);
+        FREE(params->workspaces_walk);
+        return 1;
+    }
+
     if (!ws->name || SLIST_EMPTY(outputs)) { /* Invalid state */
         I3STRING_FREE(ws->name);
         FREE(ws->canonical_name);
@@ -209,7 +221,7 @@ static int workspaces_end_map_cb(void *params_) {
         return 1;
     }
 
-    /* Handle no output case */
+    /* Handle no output case - fallback to primary */
     ws->output = get_output_by_name("primary");
     if (ws->output == NULL) {
         ws->output = SLIST_FIRST(outputs);
diff --git a/release-notes/bugfixes/2-i3bar-primary-workspace-button b/release-notes/bugfixes/2-i3bar-primary-workspace-button
new file mode 100644
index 00000000..d4d63054
--- /dev/null
+++ b/release-notes/bugfixes/2-i3bar-primary-workspace-button
@@ -0,0 +1 @@
+fix i3bar workspace buttons showing up in primary screen
diff --git a/testcases/t/555-i3bar-workspace-output-assignment.t b/testcases/t/555-i3bar-workspace-output-assignment.t
new file mode 100644
index 00000000..bc50d766
--- /dev/null
+++ b/testcases/t/555-i3bar-workspace-output-assignment.t
@@ -0,0 +1,147 @@
+#!perl
+# vim:ts=4:sw=4:expandtab
+#
+# Please read the following documents before working on tests:
+# • https://build.i3wm.org/docs/testsuite.html
+#   (or docs/testsuite)
+#
+# • https://build.i3wm.org/docs/lib-i3test.html
+#   (alternatively: perldoc ./testcases/lib/i3test.pm)
+#
+# • https://build.i3wm.org/docs/ipc.html
+#   (or docs/ipc)
+#
+# • https://i3wm.org/downloads/modern_perl_a4.pdf
+#   (unless you are already familiar with Perl)
+#
+# Verify that i3bar only shows correct workspace buttons in each output.
+# Ticket: #6560
+# Bug still in: 4.25-6-g0e2e8290
+use File::Temp qw(tempdir);
+use i3test i3_autostart => 0;
+use i3test::Util qw(slurp);
+use i3test::XTEST;
+use POSIX qw(mkfifo);
+
+################################################################################
+# Test that a bar configured for primary output only shows workspaces from that
+# output, not from other outputs.
+################################################################################
+
+# Create temp files for i3bar PID and exit signaling
+my $tmpdir = tempdir(CLEANUP => 1);
+my $pidfile = "$tmpdir/i3bar.pid";
+my $exitfifo = "$tmpdir/fifo";
+my $logfile = "$tmpdir/i3bar.log";
+mkfifo("$exitfifo", 0600) or BAIL_OUT "Could not create FIFO: $!";
+
+# Create a wrapper script that tracks i3bar's PID and signals when it exits
+my $scriptfile = "$tmpdir/i3bar-wrapper.sh";
+open(my $scriptfh, '>', $scriptfile) or BAIL_OUT "Cannot create wrapper: $!";
+print $scriptfh <<"EOF";
+#!/bin/sh
+echo "---- DEBUG: i3bar wrapper ----"
+cat "$scriptfile"
+echo '---- DEBUG: i3bar wrapper ----'
+# Use tee so that the logs also end up in the testsuite log file.
+(i3bar -V "\$@" 2>&1 | tee "$logfile") &
+echo \$! > "$pidfile"
+wait
+echo done > "$exitfifo"
+EOF
+close($scriptfh);
+chmod 0755, $scriptfile;
+
+my $config = <<"EOT";
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+fake-outputs 1024x768+0+0P,1024x768+1024+0
+workspace 1 output fake-1 # primary
+workspace 2 output fake-0 # nonprimary
+
+bar {
+    i3bar_command $scriptfile
+    output primary
+}
+EOT
+
+my $pid = launch_with_config($config);
+
+my $i3 = i3(get_socket_path());
+$i3->connect()->recv;
+my $cv = AnyEvent->condvar;
+my $timer = AnyEvent->timer(after => 1, interval => 0, cb => sub { $cv->send(0) });
+$i3->subscribe({
+        window => sub {
+            my ($event) = @_;
+            if ($event->{change} eq 'new') {
+                if (defined($event->{container}->{window_properties}->{class}) &&
+                    $event->{container}->{window_properties}->{class} eq 'i3bar') {
+                    $cv->send($event->{container});
+                }
+            }
+        },
+    })->recv;
+
+sub i3bar_present {
+    my ($nodes) = @_;
+
+    for my $node (@{$nodes}) {
+	my $props = $node->{window_properties};
+	if (defined($props) && $props->{class} eq 'i3bar') {
+	    return $node->{window};
+	}
+    }
+
+    return 0 if !@{$nodes};
+
+    my @children = (map { @{$_->{nodes}} } @{$nodes},
+                    map { @{$_->{'floating_nodes'}} } @{$nodes});
+
+    return i3bar_present(\@children);
+}
+
+my $i3bar_window = i3bar_present($i3->get_tree->recv->{nodes});
+if ($i3bar_window) {
+    ok(1, 'i3bar present');
+} else {
+    my $con = $cv->recv;
+    ok($con, 'i3bar appeared');
+    $i3bar_window = $con->{window};
+}
+
+diag('i3bar window = ' . $i3bar_window);
+xtest_sync_with_i3;
+xtest_sync_with($i3bar_window);
+
+# The actual test
+cmd 'workspace 1';
+my $win1 = open_window;
+cmd 'workspace 2';
+my $win2 = open_window;
+
+# Kill i3bar gracefully BEFORE exiting i3 to ensure buffers are flushed
+# (if i3 exits first, i3bar gets SIGPIPE and buffers are lost)
+open(my $pidfh, '<', $pidfile) or BAIL_OUT "Cannot read i3bar PID: $!";
+my $bar_pid = <$pidfh>;
+close($pidfh);
+chomp($bar_pid);
+kill('TERM', $bar_pid);
+
+# Wait for i3bar to exit by reading from the FIFO (blocks until wrapper writes)
+open(my $fifofh, '<', $exitfifo) or BAIL_OUT "Cannot open FIFO: $!";
+my $result = <$fifofh>;
+close($fifofh);
+ok(defined($result), 'i3bar ended');
+
+exit_gracefully($pid);
+
+my $log = slurp($logfile);
+
+my @ws2_draws = ($log =~ /Drawing button for WS 2 at/g);
+ok(scalar(@ws2_draws) > 0, "Workspace 2 (on primary) is drawn in the bar");
+
+my @ws1_draws = ($log =~ /Drawing button for WS 1 at/g);
+is(scalar(@ws1_draws), 0, "Workspace 1 (on non-primary) should NOT be drawn on primary bar");
+
+done_testing;
