diff --git a/build.zig b/build.zig
index 494f0ce..0c9c217 100644
--- a/build.zig
+++ b/build.zig
@@ -41,6 +41,7 @@ pub fn build(b: *zbs.Builder) !void {
const scanner = ScanProtocolsStep.create(b);
scanner.addSystemProtocol("stable/xdg-shell/xdg-shell.xml");
scanner.addProtocolPath("protocol/river-control-unstable-v1.xml");
+ scanner.addProtocolPath("protocol/river-options-unstable-v1.xml");
scanner.addProtocolPath("protocol/river-status-unstable-v1.xml");
scanner.addProtocolPath("protocol/wlr-layer-shell-unstable-v1.xml");
scanner.addProtocolPath("protocol/wlr-output-power-management-unstable-v1.xml");
@@ -85,18 +86,20 @@ pub fn build(b: *zbs.Builder) !void {
}
if (examples) {
- const status = b.addExecutable("status", "example/status.zig");
- status.setTarget(target);
- status.setBuildMode(mode);
+ inline for (.{ "status", "options" }) |example_name| {
+ const example = b.addExecutable(example_name, "example/" ++ example_name ++ ".zig");
+ example.setTarget(target);
+ example.setBuildMode(mode);
- status.step.dependOn(&scanner.step);
- status.addPackage(scanner.getPkg());
- status.linkLibC();
- status.linkSystemLibrary("wayland-client");
+ example.step.dependOn(&scanner.step);
+ example.addPackage(scanner.getPkg());
+ example.linkLibC();
+ example.linkSystemLibrary("wayland-client");
- scanner.addCSource(status);
+ scanner.addCSource(example);
- status.install();
+ example.install();
+ }
}
{
diff --git a/deps/zig-wayland b/deps/zig-wayland
index 0dd1bee..05f539c 160000
--- a/deps/zig-wayland
+++ b/deps/zig-wayland
@@ -1 +1 @@
-Subproject commit 0dd1bee2dea065d45ce674c5b3ece2fda38f775d
+Subproject commit 05f539c8934f18f93ac50aad654fa469bf5121f8
diff --git a/example/options.zig b/example/options.zig
new file mode 100644
index 0000000..52ede7a
--- /dev/null
+++ b/example/options.zig
@@ -0,0 +1,102 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2020 The River Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+const std = @import("std");
+const os = std.os;
+const mem = std.mem;
+const fmt = std.fmt;
+
+const wayland = @import("wayland");
+const wl = wayland.client.wl;
+const zriver = wayland.client.zriver;
+
+const SetupContext = struct {
+ options_manager: ?*zriver.OptionsManagerV1 = null,
+ outputs: std.ArrayList(*wl.Output) = std.ArrayList(*wl.Output).init(std.heap.c_allocator),
+};
+
+const ValueType = enum {
+ int,
+ uint,
+ fixed,
+ string,
+};
+
+/// Disclaimer, the output handling implemented here is by no means robust. A
+/// proper client should likely use xdg-output to identify outputs by name.
+///
+/// Usage: ./options output_num|NULL [ ]
+/// Examples:
+/// ./options foo
+/// ./options foo NULL uint 42
+/// ./options foo 1 string ziggy
+pub fn main() !void {
+ const display = try wl.Display.connect(null);
+ const registry = try display.getRegistry();
+
+ var context = SetupContext{};
+
+ registry.setListener(*SetupContext, registryListener, &context) catch unreachable;
+ _ = try display.roundtrip();
+
+ const options_manager = context.options_manager orelse return error.RiverOptionsManagerNotAdvertised;
+
+ const key = os.argv[1];
+ const output = if (mem.eql(u8, "NULL", mem.span(os.argv[2])))
+ null
+ else
+ context.outputs.items[fmt.parseInt(u32, mem.span(os.argv[2]), 10) catch return error.InvalidOutput];
+ const handle = try options_manager.getOptionHandle(key, output);
+ handle.setListener([*:0]u8, optionListener, key) catch unreachable;
+
+ if (os.argv.len > 3) {
+ const value_type = std.meta.stringToEnum(ValueType, mem.span(os.argv[3])) orelse return error.InvalidType;
+ switch (value_type) {
+ .int => handle.setIntValue(fmt.parseInt(i32, mem.span(os.argv[4]), 10) catch return error.InvalidInt),
+ .uint => handle.setUintValue(fmt.parseInt(u32, mem.span(os.argv[4]), 10) catch return error.InvalidUint),
+ .fixed => handle.setFixedValue(wl.Fixed.fromDouble(fmt.parseFloat(f64, mem.span(os.argv[4])) catch return error.InvalidFixed)),
+ .string => handle.setStringValue(os.argv[4]),
+ }
+ }
+
+ // Loop forever, listening for new events.
+ while (true) _ = try display.dispatch();
+}
+
+fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, context: *SetupContext) void {
+ switch (event) {
+ .global => |global| {
+ if (std.cstr.cmp(global.interface, zriver.OptionsManagerV1.getInterface().name) == 0) {
+ context.options_manager = registry.bind(global.name, zriver.OptionsManagerV1, 1) catch return;
+ } else if (std.cstr.cmp(global.interface, wl.Output.getInterface().name) == 0) {
+ const output = registry.bind(global.name, wl.Output, 1) catch return;
+ context.outputs.append(output) catch @panic("out of memory");
+ }
+ },
+ .global_remove => {},
+ }
+}
+
+fn optionListener(handle: *zriver.OptionHandleV1, event: zriver.OptionHandleV1.Event, key: [*:0]const u8) void {
+ switch (event) {
+ .unset => std.debug.print("option '{}' unset\n", .{key}),
+ .int_value => |ev| std.debug.print("option '{}' of type int has value {}\n", .{ key, ev.value }),
+ .uint_value => |ev| std.debug.print("option '{}' of type uint has value {}\n", .{ key, ev.value }),
+ .fixed_value => |ev| std.debug.print("option '{}' of type fixed has value {}\n", .{ key, ev.value.toDouble() }),
+ .string_value => |ev| std.debug.print("option '{}' of type string has value {}\n", .{ key, ev.value }),
+ }
+}
diff --git a/river/Option.zig b/river/Option.zig
new file mode 100644
index 0000000..1008d5f
--- /dev/null
+++ b/river/Option.zig
@@ -0,0 +1,135 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2020 The River Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+const Self = @This();
+
+const std = @import("std");
+const mem = std.mem;
+const meta = std.meta;
+
+const wayland = @import("wayland");
+const wl = wayland.server.wl;
+const zriver = wayland.server.zriver;
+
+const util = @import("util.zig");
+
+const Output = @import("Output.zig");
+const OptionsManager = @import("OptionsManager.zig");
+
+pub const Value = union(enum) {
+ unset: void,
+ int: i32,
+ uint: u32,
+ fixed: wl.Fixed,
+ string: ?[*:0]const u8,
+};
+
+options_manager: *OptionsManager,
+link: wl.list.Link = undefined,
+
+output: ?*Output,
+key: [*:0]const u8,
+value: Value = .unset,
+
+handles: wl.list.Head(zriver.OptionHandleV1, null) = undefined,
+
+pub fn create(options_manager: *OptionsManager, output: ?*Output, key: [*:0]const u8) !*Self {
+ const self = try util.gpa.create(Self);
+ errdefer util.gpa.destroy(self);
+
+ self.* = .{
+ .options_manager = options_manager,
+ .output = output,
+ .key = try util.gpa.dupeZ(u8, mem.span(key)),
+ };
+ self.handles.init();
+
+ options_manager.options.append(self);
+
+ return self;
+}
+
+pub fn destroy(self: *Self) void {
+ var it = self.handles.safeIterator(.forward);
+ while (it.next()) |handle| handle.destroy();
+ if (self.value == .string) if (self.value.string) |s| util.gpa.free(mem.span(s));
+ util.gpa.destroy(self);
+}
+
+/// Asserts that the new value is not .unset.
+/// Ignores the new value if the value is currently set and the type does not match.
+/// If the value is a string, the string is cloned.
+/// If the value is changed, send the proper event to all clients
+pub fn set(self: *Self, value: Value) !void {
+ std.debug.assert(value != .unset);
+ if (self.value != .unset and meta.activeTag(value) != meta.activeTag(self.value)) return;
+
+ if (self.value == .unset and value == .string) {
+ self.value = .{
+ .string = if (value.string) |s| (try util.gpa.dupeZ(u8, mem.span(s))).ptr else null,
+ };
+ } else if (self.value == .string and
+ // TODO: std.mem needs a good way to compare optional sentinel pointers
+ ((self.value.string == null and value.string == null) or
+ (self.value.string != null and value.string != null and
+ std.cstr.cmp(self.value.string.?, value.string.?) != 0)))
+ {
+ const owned_string = if (value.string) |s| (try util.gpa.dupeZ(u8, mem.span(s))).ptr else null;
+ if (self.value.string) |s| util.gpa.free(mem.span(s));
+ self.value.string = owned_string;
+ } else if (self.value == .unset or (self.value != .string and !std.meta.eql(self.value, value))) {
+ self.value = value;
+ } else {
+ // The value was not changed
+ return;
+ }
+
+ var it = self.handles.iterator(.forward);
+ while (it.next()) |handle| self.sendValue(handle);
+}
+
+fn sendValue(self: Self, handle: *zriver.OptionHandleV1) void {
+ switch (self.value) {
+ .unset => handle.sendUnset(),
+ .int => |v| handle.sendIntValue(v),
+ .uint => |v| handle.sendUintValue(v),
+ .fixed => |v| handle.sendFixedValue(v),
+ .string => |v| handle.sendStringValue(v),
+ }
+}
+
+pub fn addHandle(self: *Self, handle: *zriver.OptionHandleV1) void {
+ self.handles.append(handle);
+ self.sendValue(handle);
+ handle.setHandler(*Self, handleRequest, handleDestroy, self);
+}
+
+fn handleRequest(handle: *zriver.OptionHandleV1, request: zriver.OptionHandleV1.Request, self: *Self) void {
+ switch (request) {
+ .destroy => handle.destroy(),
+ .set_int_value => |req| self.set(.{ .int = req.value }) catch unreachable,
+ .set_uint_value => |req| self.set(.{ .uint = req.value }) catch unreachable,
+ .set_fixed_value => |req| self.set(.{ .fixed = req.value }) catch unreachable,
+ .set_string_value => |req| self.set(.{ .string = req.value }) catch {
+ handle.getClient().postNoMemory();
+ },
+ }
+}
+
+fn handleDestroy(handle: *zriver.OptionHandleV1, self: *Self) void {
+ handle.getLink().remove();
+}
diff --git a/river/OptionsManager.zig b/river/OptionsManager.zig
new file mode 100644
index 0000000..a3e2876
--- /dev/null
+++ b/river/OptionsManager.zig
@@ -0,0 +1,100 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2020 The River Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+const Self = @This();
+
+const std = @import("std");
+
+const wayland = @import("wayland");
+const wl = wayland.server.wl;
+const zriver = wayland.server.zriver;
+
+const wlr = @import("wlroots");
+
+const util = @import("util.zig");
+
+const Option = @import("Option.zig");
+const Output = @import("Output.zig");
+const Server = @import("Server.zig");
+
+global: *wl.Global,
+server_destroy: wl.Listener(*wl.Server) = wl.Listener(*wl.Server).init(handleServerDestroy),
+
+options: wl.list.Head(Option, "link") = undefined,
+
+pub fn init(self: *Self, server: *Server) !void {
+ self.* = .{
+ .global = try wl.Global.create(server.wl_server, zriver.OptionsManagerV1, 1, *Self, self, bind),
+ };
+ self.options.init();
+ server.wl_server.addDestroyListener(&self.server_destroy);
+}
+
+fn handleServerDestroy(listener: *wl.Listener(*wl.Server), wl_server: *wl.Server) void {
+ const self = @fieldParentPtr(Self, "server_destroy", listener);
+ self.global.destroy();
+ var it = self.options.safeIterator(.forward);
+ while (it.next()) |option| option.destroy();
+}
+
+fn bind(client: *wl.Client, self: *Self, version: u32, id: u32) callconv(.C) void {
+ const options_manager = zriver.OptionsManagerV1.create(client, 1, id) catch {
+ client.postNoMemory();
+ return;
+ };
+ options_manager.setHandler(*Self, handleRequest, null, self);
+}
+
+fn handleRequest(
+ options_manager: *zriver.OptionsManagerV1,
+ request: zriver.OptionsManagerV1.Request,
+ self: *Self,
+) void {
+ switch (request) {
+ .destroy => options_manager.destroy(),
+ .get_option_handle => |req| {
+ const output = if (req.output) |wl_output| blk: {
+ // Ignore if the wl_output is inert
+ const wlr_output = wlr.Output.fromWlOutput(wl_output) orelse return;
+ break :blk @intToPtr(*Output, wlr_output.data);
+ } else null;
+
+ // Look for an existing Option, if not found create a new one
+ var it = self.options.iterator(.forward);
+ const option = while (it.next()) |option| {
+ if (option.output == output and std.cstr.cmp(option.key, req.key) == 0) {
+ break option;
+ }
+ } else
+ Option.create(self, output, req.key) catch {
+ options_manager.getClient().postNoMemory();
+ return;
+ };
+
+ const handle = zriver.OptionHandleV1.create(
+ options_manager.getClient(),
+ options_manager.getVersion(),
+ req.handle,
+ ) catch {
+ options_manager.getClient().postNoMemory();
+ return;
+ };
+
+ option.addHandle(handle);
+ },
+ }
+}
diff --git a/river/Server.zig b/river/Server.zig
index a5f7ca0..7923065 100644
--- a/river/Server.zig
+++ b/river/Server.zig
@@ -27,6 +27,7 @@ const log = @import("log.zig");
const util = @import("util.zig");
const Config = @import("Config.zig");
+const OptionsManager = @import("OptionsManager.zig");
const Control = @import("Control.zig");
const DecorationManager = @import("DecorationManager.zig");
const InputManager = @import("InputManager.zig");
@@ -63,6 +64,7 @@ root: Root,
config: Config,
control: Control,
status_manager: StatusManager,
+options_manager: OptionsManager,
pub fn init(self: *Self) !void {
self.wl_server = try wl.Server.create();
@@ -115,6 +117,7 @@ pub fn init(self: *Self) !void {
try self.input_manager.init(self);
try self.control.init(self);
try self.status_manager.init(self);
+ try self.options_manager.init(self);
// These all free themselves when the wl_server is destroyed
_ = try wlr.DataDeviceManager.create(self.wl_server);