gtk: harden quick-terminal output-order state handling

Install Wayland protocol listeners at bind time so late-added globals
still receive events and listener setup stays tied to object lifetime.

Track whether kde_output_order_v1 emitted any outputs in a cycle and
clear cached primary-output state on empty or invalid updates. Also
reset this cycle tracking when the protocol global is removed to avoid
stale monitor selection.
This commit is contained in:
Jake Guthmiller
2026-02-13 21:26:34 -06:00
parent 630c2dff19
commit e25d8a6f2f

View File

@@ -50,6 +50,10 @@ pub const App = struct {
/// Initialized to true so the first event after binding is captured.
output_order_done: bool = true,
/// True if we've received an `output` event in the current cycle.
/// This lets us detect empty cycles and clear stale cached state.
output_order_seen_output: bool = false,
default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null,
xdg_activation: ?*xdg.ActivationV1 = null,
@@ -100,15 +104,9 @@ pub const App = struct {
registry.setListener(*Context, registryListener, context);
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
// Set up listeners for protocols that send events on bind.
// All listeners must be set before the roundtrip so that
// events aren't lost.
if (context.kde_decoration_manager) |deco_manager| {
deco_manager.setListener(*Context, decoManagerListener, context);
}
if (context.kde_output_order) |output_order| {
output_order.setListener(*Context, outputOrderListener, context);
}
// Do another roundtrip to process events emitted by globals we bound
// during registry discovery (e.g. default decoration mode, output
// order). Listeners are installed at bind time in registryListener.
if (context.kde_decoration_manager != null or context.kde_output_order != null) {
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
}
@@ -269,7 +267,15 @@ pub const App = struct {
) == .eq) {
log.debug("matched {}", .{T});
@field(context, field.name) = registry.bind(
if (@field(context, field.name) != null) {
log.warn(
"duplicate global for {s}; keeping existing binding",
.{v.interface},
);
break;
}
const global = registry.bind(
v.name,
T,
T.generated_version,
@@ -280,6 +286,22 @@ pub const App = struct {
);
return;
};
@field(context, field.name) = global;
// Install listeners immediately at bind time. This
// keeps listener setup and object lifetime in one
// place and also supports globals that appear later.
if (comptime std.mem.eql(u8, field.name, "kde_decoration_manager")) {
const deco_manager: *org.KdeKwinServerDecorationManager =
@field(context, field.name).?;
deco_manager.setListener(*Context, decoManagerListener, context);
}
if (comptime std.mem.eql(u8, field.name, "kde_output_order")) {
const output_order: *kde.OutputOrderV1 =
@field(context, field.name).?;
output_order.setListener(*Context, outputOrderListener, context);
}
break;
}
}
},
@@ -301,6 +323,7 @@ pub const App = struct {
context.primary_output_name = null;
context.primary_output_match_failed_logged = false;
context.output_order_done = true;
context.output_order_seen_output = false;
}
break :remove;
}
@@ -331,14 +354,24 @@ pub const App = struct {
.output => |v| {
if (context.output_order_done) {
context.output_order_done = false;
context.output_order_seen_output = true;
const name = std.mem.sliceTo(v.output_name, 0);
if (name.len <= 63) {
if (name.len == 0) {
context.primary_output_name = null;
context.primary_output_match_failed_logged = false;
log.warn(
"ignoring empty primary output name from kde_output_order_v1",
.{},
);
} else if (name.len <= 63) {
var buf: [63:0]u8 = @splat(0);
@memcpy(buf[0..name.len], name);
context.primary_output_name = buf;
context.primary_output_match_failed_logged = false;
log.debug("primary output: {s}", .{name});
} else {
context.primary_output_name = null;
context.primary_output_match_failed_logged = false;
log.warn(
"ignoring primary output name longer than 63 bytes from kde_output_order_v1",
.{},
@@ -347,7 +380,14 @@ pub const App = struct {
}
},
.done => {
// An empty update means the compositor currently reports no
// outputs in priority order, so drop any stale cached primary.
if (!context.output_order_seen_output) {
context.primary_output_name = null;
context.primary_output_match_failed_logged = false;
}
context.output_order_done = true;
context.output_order_seen_output = false;
},
}
}