From 8cbccbfb6e377d6e12b951a0713ef5eb3dcdc2b6 Mon Sep 17 00:00:00 2001 From: Isaac Freund Date: Fri, 15 Jan 2021 19:54:19 +0100 Subject: [PATCH] river-options: implement --- build.zig | 21 +++--- deps/zig-wayland | 2 +- example/options.zig | 102 +++++++++++++++++++++++++++++ river/Option.zig | 135 +++++++++++++++++++++++++++++++++++++++ river/OptionsManager.zig | 100 +++++++++++++++++++++++++++++ river/Server.zig | 3 + 6 files changed, 353 insertions(+), 10 deletions(-) create mode 100644 example/options.zig create mode 100644 river/Option.zig create mode 100644 river/OptionsManager.zig 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);