river-options: rework, bump to v2

Options are now all global but may be overridden per-output. If an
output local value is requested but none has been set, the global value
is provided instead. This makes for much better ergonomics when
configuring layout related options in particular.
This commit is contained in:
Isaac Freund 2021-04-15 00:28:39 +02:00
parent 33fb7725c5
commit d08032d685
19 changed files with 919 additions and 734 deletions

View file

@ -64,7 +64,7 @@ pub fn build(b: *zbs.Builder) !void {
scanner.addSystemProtocol("unstable/xdg-output/xdg-output-unstable-v1.xml"); scanner.addSystemProtocol("unstable/xdg-output/xdg-output-unstable-v1.xml");
scanner.addSystemProtocol("unstable/pointer-constraints/pointer-constraints-unstable-v1.xml"); scanner.addSystemProtocol("unstable/pointer-constraints/pointer-constraints-unstable-v1.xml");
scanner.addProtocolPath("protocol/river-control-unstable-v1.xml"); scanner.addProtocolPath("protocol/river-control-unstable-v1.xml");
scanner.addProtocolPath("protocol/river-options-unstable-v1.xml"); scanner.addProtocolPath("protocol/river-options-v2.xml");
scanner.addProtocolPath("protocol/river-status-unstable-v1.xml"); scanner.addProtocolPath("protocol/river-status-unstable-v1.xml");
scanner.addProtocolPath("protocol/river-layout-v1.xml"); scanner.addProtocolPath("protocol/river-layout-v1.xml");
scanner.addProtocolPath("protocol/wlr-layer-shell-unstable-v1.xml"); scanner.addProtocolPath("protocol/wlr-layer-shell-unstable-v1.xml");

View file

@ -45,6 +45,7 @@ function __riverctl_completion ()
declare-option \ declare-option \
get-option \ get-option \
set-option \ set-option \
unset-option \
mod-option" mod-option"
COMPREPLY=($(compgen -W "${OPTS}" -- "${COMP_WORDS[1]}")) COMPREPLY=($(compgen -W "${OPTS}" -- "${COMP_WORDS[1]}"))
elif [ "${COMP_CWORD}" -eq 2 ] elif [ "${COMP_CWORD}" -eq 2 ]
@ -56,7 +57,7 @@ function __riverctl_completion ()
"map"|"unmap") OPTS="-release" ;; "map"|"unmap") OPTS="-release" ;;
"attach-mode") OPTS="top bottom" ;; "attach-mode") OPTS="top bottom" ;;
"focus-follows-cursor") OPTS="disabled normal strict" ;; "focus-follows-cursor") OPTS="disabled normal strict" ;;
"declare-option"|"get-option"|"set-option"|"mod-option") OPTS="-output -focused-output" ;; "get-option"|"set-option"|"unset-option"|"mod-option") OPTS="-output -focused-output" ;;
*) return ;; *) return ;;
esac esac
COMPREPLY=($(compgen -W "${OPTS}" -- "${COMP_WORDS[2]}")) COMPREPLY=($(compgen -W "${OPTS}" -- "${COMP_WORDS[2]}"))
@ -66,4 +67,3 @@ function __riverctl_completion ()
} }
complete -F __riverctl_completion riverctl complete -F __riverctl_completion riverctl

View file

@ -1,6 +1,6 @@
function __fish_riverctl_complete_no_subcommand function __fish_riverctl_complete_no_subcommand
for i in (commandline -opc) for i in (commandline -opc)
if contains -- $i close csd-filter-add exit float-filter-add focus-output focus-view layout mod-main-count mod-main-factor move resize snap send-to-output spawn swap toggle-float toggle-fullscreen zoom set-focused-tags set-view-tags toggle-focused-tags toggle-view-tags spawn-tagmask declare-mode enter-mode map map-pointer unmap unmap-pointer attach-mode background-color border-color-focused border-color-unfocused border-width focus-follows-cursor opacity outer-padding set-repeat view-padding xcursor-theme declare-option get-option set-option mod-option output_title if contains -- $i close csd-filter-add exit float-filter-add focus-output focus-view layout mod-main-count mod-main-factor move resize snap send-to-output spawn swap toggle-float toggle-fullscreen zoom set-focused-tags set-view-tags toggle-focused-tags toggle-view-tags spawn-tagmask declare-mode enter-mode map map-pointer unmap unmap-pointer attach-mode background-color border-color-focused border-color-unfocused border-width focus-follows-cursor opacity outer-padding set-repeat view-padding xcursor-theme declare-option get-option set-option unset-option mod-option output_title
return 1 return 1
end end
end end
@ -55,6 +55,7 @@ complete -c riverctl -x -n '__fish_riverctl_complete_no_subcommand' -a xcursor-t
complete -c riverctl -x -n '__fish_riverctl_complete_no_subcommand' -a declare-option -d 'Declare a new option with the given type and initial value' complete -c riverctl -x -n '__fish_riverctl_complete_no_subcommand' -a declare-option -d 'Declare a new option with the given type and initial value'
complete -c riverctl -x -n '__fish_riverctl_complete_no_subcommand' -a get-option -d 'Print the current value of the given option to stdout' complete -c riverctl -x -n '__fish_riverctl_complete_no_subcommand' -a get-option -d 'Print the current value of the given option to stdout'
complete -c riverctl -x -n '__fish_riverctl_complete_no_subcommand' -a set-option -d 'Set the value of the specified option' complete -c riverctl -x -n '__fish_riverctl_complete_no_subcommand' -a set-option -d 'Set the value of the specified option'
complete -c riverctl -x -n '__fish_riverctl_complete_no_subcommand' -a unset-option -d 'Unset the value of the specified option for the given output'
complete -c riverctl -x -n '__fish_riverctl_complete_no_subcommand' -a mod-option -d 'Add value to the value of the specified option' complete -c riverctl -x -n '__fish_riverctl_complete_no_subcommand' -a mod-option -d 'Add value to the value of the specified option'
complete -c riverctl -x -n '__fish_riverctl_complete_no_subcommand' -a output_title -d 'Changing this option changes the title of the Wayland and X11 backend outputs' complete -c riverctl -x -n '__fish_riverctl_complete_no_subcommand' -a output_title -d 'Changing this option changes the title of the Wayland and X11 backend outputs'
@ -71,8 +72,7 @@ complete -c riverctl -x -n '__fish_seen_subcommand_from map' -a
complete -c riverctl -x -n '__fish_seen_subcommand_from unmap' -a '-release' complete -c riverctl -x -n '__fish_seen_subcommand_from unmap' -a '-release'
complete -c riverctl -x -n '__fish_seen_subcommand_from attach-mode' -a 'top bottom' complete -c riverctl -x -n '__fish_seen_subcommand_from attach-mode' -a 'top bottom'
complete -c riverctl -x -n '__fish_seen_subcommand_from focus-follows-cursor' -a 'disabled normal strict' complete -c riverctl -x -n '__fish_seen_subcommand_from focus-follows-cursor' -a 'disabled normal strict'
complete -c riverctl -x -n '__fish_seen_subcommand_from declare-option' -a '-output -focused-output'
complete -c riverctl -x -n '__fish_seen_subcommand_from get-option' -a '-output -focused-output' complete -c riverctl -x -n '__fish_seen_subcommand_from get-option' -a '-output -focused-output'
complete -c riverctl -x -n '__fish_seen_subcommand_from set-option' -a '-output -focused-output' complete -c riverctl -x -n '__fish_seen_subcommand_from set-option' -a '-output -focused-output'
complete -c riverctl -x -n '__fish_seen_subcommand_from unset-option' -a '-output -focused-output'
complete -c riverctl -x -n '__fish_seen_subcommand_from mod-option' -a '-output -focused-output' complete -c riverctl -x -n '__fish_seen_subcommand_from mod-option' -a '-output -focused-output'

View file

@ -61,6 +61,7 @@ _riverctl() {
'declare-option:Declare a new option with the given type and initial value' 'declare-option:Declare a new option with the given type and initial value'
'get-option:Print the current value of the given option to stdout' 'get-option:Print the current value of the given option to stdout'
'set-option:Set the value of the specified option' 'set-option:Set the value of the specified option'
'unset-option:Unset the value of the specified option for the given output'
'mod-option:Add value to the value of the specified option' 'mod-option:Add value to the value of the specified option'
'output_title:Changing this option changes the title of the Wayland and X11 backend outputs' 'output_title:Changing this option changes the title of the Wayland and X11 backend outputs'
) )
@ -87,13 +88,12 @@ _riverctl() {
unmap) _alternative 'arguments:optional:(-release)' ;; unmap) _alternative 'arguments:optional:(-release)' ;;
attach-mode) _attach ;; attach-mode) _attach ;;
focus-follows-cursor) _focus_cursor ;; focus-follows-cursor) _focus_cursor ;;
declare-option) _river_opts ;;
get-option) _river_opts ;; get-option) _river_opts ;;
set-option) _river_opts ;; set-option) _river_opts ;;
unset-option) _river_opts ;;
mod-option) _river_opts ;; mod-option) _river_opts ;;
*) return 0 ;; *) return 0 ;;
esac esac
return 1 return 1
} }

View file

@ -259,11 +259,15 @@ scoped either globally or per-output if the *-output* flag is passed with the
name of the output as obtained from the xdg-output protocol. Alternatively, name of the output as obtained from the xdg-output protocol. Alternatively,
the currently focused output may be targeted with the *-focused-output* flag. the currently focused output may be targeted with the *-focused-output* flag.
*declare-option* [*-output* _output_name_|*-focused-output*] _name_ _type_ _value_ *declare-option* _name_ _type_ _value_
Declare a new option with the given _type_ and initial _value_. If Declare a new option with the given _type_ and initial _value_. If
the option already exists with the given _type_, it is still set the option already exists, this command does nothing. The following
to _value_. If the option already exists with a different type, types are available:
nothing happens.
- _int_: a signed 32-bit integer
- _uint_: an unsigned 32-bit integer
- _fixed_: a signed 24.8 bit fixed point number
- _string_: a string of bytes, may be null
*get-option* [*-output* _output_name_|*-focused-output*] _name_ *get-option* [*-output* _output_name_|*-focused-output*] _name_
Print the current value of the given option to stdout. Print the current value of the given option to stdout.
@ -271,36 +275,24 @@ the currently focused output may be targeted with the *-focused-output* flag.
*set-option* [*-output* _output_name_|*-focused-output*] _name_ _value_ *set-option* [*-output* _output_name_|*-focused-output*] _name_ _value_
Set the value of the specified option to _value_. Set the value of the specified option to _value_.
*unset-option* (*-output* _output_name_|*-focused-output*) _name_
Unset the value of the specified option for the given output and
cause it to fall back to the global value. Either the *-output* or
*-focused-output* flag is required.
*mod-option* [*-output* _output_name_|*-focused-output*] _name_ _value_ *mod-option* [*-output* _output_name_|*-focused-output*] _name_ _value_
Add _value_ to the value of the specified option. _value_ can be negative. Add _value_ to the value of the specified option. _value_ can be negative.
River declares certain default options for all outputs. River declares certain default options which will always be available:
*output_title* (string)
Changing this option changes the title of the wayland and X11 backend
outputs.
*layout* (string) *layout* (string)
The layout namespace used to determine which layout should arrange this The layout namespace used to determine which layout should arrange this
output. If set to null or no layout with this namespace exists for this output. If set to null or no layout with this namespace exists for this
output, the output will enter floating mode. Defaults to null. output, the output will enter floating mode. Defaults to null.
*main_amount* (uint, optional hint for layouts) *output_title* (string)
An arbitrary positive integer indicating the amount of main views. Defaults Changing this option changes the title of the wayland and X11 backend
to 1. outputs.
*main_factor* (float, optional hint for layouts)
A floating point numger indicating the relative size of the area reserved
for main views. Note that layouts commonly expect values between 0.1 and 0.9.
Defaults to 0.6.
*view_padding* (uint, optional hint for layouts)
A positive integer indicating the padding in of pixels between / around
views. Defaults to 10.
*outer_padding* (uint, optional hint for layouts)
A positive integer indicating the padding in of pixels around the layut.
Defaults to 10.
# EXAMPLES # EXAMPLES

View file

@ -10,15 +10,31 @@ rivertile - Tiled layout generator for river
# DESCRIPTION # DESCRIPTION
*rivertile* is a layout client for river. It provides four tiled layouts per *rivertile* is a layout client for river. It provides a simple tiled layout
output with split main/secondary stacks with the main area in different split main/secondary stacks.
positions.
The namespaces of the four layouts are "tile-top", "tile-right", "tile-bottom" # OPTIONS
and "tile-left", corresponding to the position of the main area.
*rivertile* uses the *main_amount*, *main_factor*, *view_padding* and These options may be set using *riverctl*(1) or another river-options
*outer_padding* options. wayland client. *rivertile* declares these options on startup, so setting
these options before starting rivertile requires them to be declared manually.
*main_location* (string, default "top")
The location of the main area. Vaild locations are "top", "bottom",
"left", and "right".
*main_count* (uint, default 1)
The number of main views.
*main_factor* (fixed, default 0.6)
The percentage of the layout area reserved for main views. *rivertle*
clamps this to the range `[0.1, 0.9]`.
*view_padding* (uint, default 6)
Padding around every view in pixels.
*outer_padding* (uint, default 6)
Padding around the edge of the layout area in pixels.
# AUTHORS # AUTHORS
@ -29,4 +45,3 @@ source contributors. For more information about river's development, see
# SEE ALSO # SEE ALSO
*river*(1), *riverctl*(1) *river*(1), *riverctl*(1)

View file

@ -1,11 +1,12 @@
#!/bin/sh #!/bin/sh
# This is the example configuration file for river(1). # This is the example configuration file for river.
# #
# If you wish to edit this, you will probably want to copy it to # If you wish to edit this, you will probably want to copy it to
# $XDG_CONFIG_HOME/river/init or $HOME/.config/river/init first. # $XDG_CONFIG_HOME/river/init or $HOME/.config/river/init first.
# #
# See the riverctl(1) man page for complete documentation # See the river(1), riverctl(1), and river(1) man pages for complete
# documentation.
# Use the "logo" key as the primary modifier # Use the "logo" key as the primary modifier
mod="Mod4" mod="Mod4"
@ -39,6 +40,16 @@ riverctl map normal $mod+Shift Comma send-to-output previous
# Mod+Return to bump the focused view to the top of the layout stack # Mod+Return to bump the focused view to the top of the layout stack
riverctl map normal $mod Return zoom riverctl map normal $mod Return zoom
# Mod+H and Mod+L to decrease/increase the main_factor option by 0.05
# rivertile(1) uses this option to determine the width of the main stack.
riverctl map normal $mod H spawn riverctl mod-option -focused-output main_factor -0.05
riverctl map normal $mod L spawn riverctl mod-option -focused-output main_factor +0.05
# Mod+Shift+H and Mod+Shift+L to increment/decrement the main_count option.
# rivertile(1) uses this option to determine the number of "main" views in the layout.
riverctl map normal $mod+Shift H spawn riverctl mod-option -focused-output main_count +1
riverctl map normal $mod+Shift L spawn riverctl mod-option -focused-output main_count -1
# Mod+Alt+{H,J,K,L} to move views # Mod+Alt+{H,J,K,L} to move views
riverctl map normal $mod+Mod1 H move left 100 riverctl map normal $mod+Mod1 H move left 100
riverctl map normal $mod+Mod1 J move down 100 riverctl map normal $mod+Mod1 J move down 100
@ -93,10 +104,10 @@ riverctl map normal $mod Space toggle-float
riverctl map normal $mod F toggle-fullscreen riverctl map normal $mod F toggle-fullscreen
# Mod+{Up,Right,Down,Left} to change layout orientation # Mod+{Up,Right,Down,Left} to change layout orientation
riverctl map normal $mod Up spawn riverctl set-option -focused-output layout tile-up riverctl map normal $mod Up spawn riverctl set-option -focused-output main_location top
riverctl map normal $mod Right spawn riverctl set-option -focused-output layout tile-right riverctl map normal $mod Right spawn riverctl set-option -focused-output main_location right
riverctl map normal $mod Down spawn riverctl set-option -focused-output layout tile-down riverctl map normal $mod Down spawn riverctl set-option -focused-output main_location bottom
riverctl map normal $mod Left spawn riverctl set-option -focused-output layout tile-left riverctl map normal $mod Left spawn riverctl set-option -focused-output main_location left
# Declare a passthrough mode. This mode has only a single mapping to return to # Declare a passthrough mode. This mode has only a single mapping to return to
# normal mode. This makes it useful for testing a nested wayland compositor # normal mode. This makes it useful for testing a nested wayland compositor
@ -134,18 +145,6 @@ done
# Set repeat rate # Set repeat rate
riverctl set-repeat 50 300 riverctl set-repeat 50 300
# Set the layout on startup
riverctl spawn rivertile
riverctl set-option -focused-output layout tile-left
# Mod+Alt+{1..9} to set main amount
# Mod+Alt+Ctrl+{1..9} to set main factor
#for i in $(seq 1 9)
#do
# riverctl map normal $mod+mod1 spawn riverctl set-option -focused-output main_amount "${i}"
# riverctl map normal $mod+Control+mod1 spawn riverctl set-option -focused-output main_factor "0.${i}"
#done
# Set app-ids of views which should float # Set app-ids of views which should float
riverctl float-filter-add "float" riverctl float-filter-add "float"
riverctl float-filter-add "popup" riverctl float-filter-add "popup"
@ -155,3 +154,8 @@ riverctl csd-filter-add "gedit"
# Set opacity and fade effect # Set opacity and fade effect
# riverctl opacity 1.0 0.75 0.0 0.1 20 # riverctl opacity 1.0 0.75 0.0 0.1 20
# Exec into the default layout generator, rivertile.
# River will send the process group of the init executable SIGTERM on exit.
riverctl set-option layout rivertile
exec rivertile

View file

@ -1,102 +0,0 @@
// 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 <https://www.gnu.org/licenses/>.
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 <key> output_num|NULL [<value_type> <value>]
/// 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 }),
}
}

View file

@ -1,162 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="river_options_unstable_v1">
<copyright>
Copyright 2020 The River Developers
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
</copyright>
<interface name="zriver_options_manager_v1" version="1">
<description summary="set and retrieve options">
This protocol allows clients to access a typed key-value store of
options. These options are identified by string keys and are scoped
either globally or per-output. This protocol does not define any
semantic meaning of the options, that is left up to compositors.
Compositors are free to set options themselves at any time, though
the type of any given option is immutable once set.
Options may never be unset once set.
</description>
<request name="destroy" type="destructor">
<description summary="destroy the zriver_options_manager_v1 object">
This request indicates that the client will not use the manager object
any more. Objects that have been created through this instance are
not affected.
</description>
</request>
<request name="get_option_handle">
<description summary="get an option handle for the given key">
If the output argument is non-null, the option is local to the given
output. Otherwise it is considered global.
</description>
<arg name="key" type="string"/>
<arg name="output" type="object" interface="wl_output" allow-null="true"/>
<arg name="handle" type="new_id" interface="zriver_option_handle_v1"/>
</request>
</interface>
<interface name="zriver_option_handle_v1" version="1">
<description summary="handle to an option">
On binding this object, one of the events will immediately be sent by
the server to inform the client of the current state of the option. New
events will be sent as the state changes.
</description>
<request name="destroy" type="destructor">
<description summary="destroy the handle">
This request indicates that the client will not use the
zriver_option_handle_v1 any more and that it may be safely destroyed.
</description>
</request>
<event name="unset">
<description summary="the option is currently unset">
The option with this key has never been set, so the first set_*_value
request received from any client will determine its type.
This can only ever be sent as the first event after binding this
interface as options cannot be unset once set.
</description>
</event>
<event name="int_value">
<description summary="the current value of the int option">
This indicates to the client that the option is of type int as well
as the current value of the option. Once set the type of the option
can never change.
</description>
<arg name="value" type="int"/>
</event>
<event name="uint_value">
<description summary="the current value of the uint option">
This indicates to the client that the option is of type uint as well
as the current value of the option. Once set the type of the option
can never change.
</description>
<arg name="value" type="uint"/>
</event>
<event name="fixed_value">
<description summary="the current value of the fixed option">
This indicates to the client that the option is of type fixed as
well as the current value of the option. Once set the type of the option
can never change.
</description>
<arg name="value" type="fixed"/>
</event>
<event name="string_value">
<description summary="the current value of the string option">
This indicates to the client that the option is of type string as
well as the current value of the option. Once set the type of the option
can never change.
</description>
<arg name="value" type="string" allow-null="true"/>
</event>
<request name="set_int_value">
<description summary="set the value of the option">
If the option is either unset or set to a value of type int, this
request asks the compositor to set the value of the option as well
as the type if previously unset. The compositor is not required to
honor this request.
If the option is already set and is not of type int, this request does nothing.
</description>
<arg name="value" type="int"/>
</request>
<request name="set_uint_value">
<description summary="set the value of the option">
If the option is either unset or set to a value of type uint, this
request asks the compositor to set the value of the option as well
as the type if previously unset. The compositor is not required to
honor this request.
If the option is already set and is not of type uint, this request
does nothing.
</description>
<arg name="value" type="uint"/>
</request>
<request name="set_fixed_value">
<description summary="set the value of the option">
If the option is either unset or set to a value of type fixed, this
request asks the compositor to set the value of the option as well
as the type if previously unset. The compositor is not required to
honor this request.
If the option is already set and is not of type fixed, this request
does nothing.
</description>
<arg name="value" type="fixed"/>
</request>
<request name="set_string_value">
<description summary="set the value of the option">
If the option is either unset or set to a value of type string,
this request asks the compositor to set the value of the option as
well as the type if previously unset. The compositor is not required
to honor this request.
If the option is already set and is not of type string, this request
does nothing.
</description>
<arg name="value" type="string" allow-null="true"/>
</request>
</interface>
</protocol>

View file

@ -0,0 +1,209 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="river_options_v2">
<copyright>
Copyright 2020-2021 The River Developers
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
</copyright>
<description summary="set and retrieve options">
This protocol allows clients to access a typed key-value store of
options. These options are global but may be overridden using a handle
scoped to a wl_output. If no output scoped value has been set, then the
global value is provided to this handle.
This protocol does not define any semantic meaning of the options,
that is left up to compositors.
Compositors are free to set or declare options themselves at any time,
though the type of any given option is immutable once set.
Options are never removed once declared.
Warning! The protocol described in this file is currently in the
testing phase. Backward compatible changes may be added together with
the corresponding interface version bump. Backward incompatible changes
can only be done by creating a new major version of the extension.
</description>
<interface name="river_options_manager_v2" version="1">
<description summary="declare options and get handles">
This interface allows clients to declare new options and create
river_option_v2 handle objects in order to retrieve the current
value or set a new one.
</description>
<request name="destroy" type="destructor">
<description summary="destroy the river_options_manager_v2 object">
This request indicates that the client will not use the manager object
any more. Objects that have been created through this instance are
not affected.
</description>
</request>
<request name="declare_int_option">
<description summary="declare a new option">
The option is created in the global scope and is initialized with the
provided value. This request is ignored if the option already exists.
</description>
<arg name="key" type="string"/>
<arg name="value" type="int"/>
</request>
<request name="declare_uint_option">
<description summary="declare a new option">
The option is created in the global scope and is initialized with the
provided value. This request is ignored if the option already exists.
</description>
<arg name="key" type="string"/>
<arg name="value" type="uint"/>
</request>
<request name="declare_string_option">
<description summary="declare a new option">
The option is created in the global scope and is initialized with the
provided value. This request is ignored if the option already exists.
</description>
<arg name="key" type="string"/>
<arg name="value" type="string" allow-null="true"/>
</request>
<request name="declare_fixed_option">
<description summary="declare a new option">
The option is created in the global scope and is initialized with the
provided value. This request is ignored if the option already exists.
</description>
<arg name="key" type="string"/>
<arg name="value" type="fixed"/>
</request>
<request name="get_option_handle">
<description summary="get an option handle for the given key">
If the output argument is non-null, the option is local to the given
output. Otherwise it is considered global.
</description>
<arg name="key" type="string"/>
<arg name="output" type="object" interface="wl_output" allow-null="true"/>
<arg name="handle" type="new_id" interface="river_option_handle_v2"/>
</request>
<request name="unset_option">
<description summary="unset an output-local value if any">
This causes the value of the option for the given output to fall
back to the global value.
</description>
<arg name="key" type="string"/>
<arg name="output" type="object" interface="wl_output"/>
</request>
</interface>
<interface name="river_option_handle_v2" version="1">
<description summary="handle to an option">
On binding this object, one of the events will immediately be sent by
the server to inform the client of the current state of the option,
including its type. Making one of the 4 set requests before receiving
this first event would be a bug as the client would not yet know the
type of the option. New events will be sent as the state changes.
</description>
<request name="destroy" type="destructor">
<description summary="destroy the handle">
This request indicates that the client will not use the
river_option_handle_v2 any more and that it may be safely destroyed.
</description>
</request>
<enum name="error">
<entry name="request_while_undeclared" value="0" summary="a request other
than destroy was made after receiving the undeclared event"/>
<entry name="type_mismatch" value="1"
summary="a set request of the wrong type was made"/>
</enum>
<event name="undeclared">
<description summary="the option has never been declared">
No option with the the given name has ever been declared. All requests
on this object aside from the destroy request are a protocol error and
no further events will be sent.
</description>
</event>
<event name="int_value">
<description summary="the current value of the int option">
This indicates to the client that the option is of type int as well
as the current value of the option. Once set the type of the option
can never change.
</description>
<arg name="value" type="int"/>
</event>
<event name="uint_value">
<description summary="the current value of the uint option">
This indicates to the client that the option is of type uint as well
as the current value of the option. Once set the type of the option
can never change.
</description>
<arg name="value" type="uint"/>
</event>
<event name="string_value">
<description summary="the current value of the string option">
This indicates to the client that the option is of type string as well
as the current value of the option. Once set the type of the option
can never change.
</description>
<arg name="value" type="string" allow-null="true"/>
</event>
<event name="fixed_value">
<description summary="the current value of the fixed option">
This indicates to the client that the option is of type fixed as well
as the current value of the option. Once set the type of the option
can never change.
</description>
<arg name="value" type="fixed"/>
</event>
<request name="set_int_value">
<description summary="set the value of the option">
If the option is of type int, set the value of the option.
Otherwise, this request is a protocol error.
</description>
<arg name="value" type="int"/>
</request>
<request name="set_uint_value">
<description summary="set the value of the option">
If the option is of type uint, set the value of the option.
Otherwise, this request is a protocol error.
</description>
<arg name="value" type="uint"/>
</request>
<request name="set_string_value">
<description summary="set the value of the option">
If the option is of type string, set the value of the option.
Otherwise, this request is a protocol error.
</description>
<arg name="value" type="string" allow-null="true"/>
</request>
<request name="set_fixed_value">
<description summary="set the value of the option">
If the option is of type fixed, set the value of the option.
Otherwise, this request is a protocol error.
</description>
<arg name="value" type="fixed"/>
</request>
</interface>
</protocol>

View file

@ -61,7 +61,7 @@ pub fn create(client: *wl.Client, version: u32, id: u32, output: *Output, namesp
// If the namespace matches that of the output, set the layout as // If the namespace matches that of the output, set the layout as
// the active one of the output and arrange it. // the active one of the output and arrange it.
if (output.layout_option.value.string) |current_layout| { if (output.layout_option.get().string) |current_layout| {
if (mem.eql(u8, namespace, mem.span(current_layout))) { if (mem.eql(u8, namespace, mem.span(current_layout))) {
output.pending.layout = &node.data; output.pending.layout = &node.data;
output.arrangeViews(); output.arrangeViews();

View file

@ -1,6 +1,6 @@
// This file is part of river, a dynamic tiling wayland compositor. // This file is part of river, a dynamic tiling wayland compositor.
// //
// Copyright 2020 The River Developers // Copyright 2020-2021 The River Developers
// //
// This program is free software: you can redistribute it and/or modify // 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 // it under the terms of the GNU General Public License as published by
@ -23,28 +23,30 @@ const meta = std.meta;
const wayland = @import("wayland"); const wayland = @import("wayland");
const wl = wayland.server.wl; const wl = wayland.server.wl;
const zriver = wayland.server.zriver; const river = wayland.server.river;
const util = @import("util.zig"); const util = @import("util.zig");
const Output = @import("Output.zig"); const Output = @import("Output.zig");
const OptionsManager = @import("OptionsManager.zig"); const OptionsManager = @import("OptionsManager.zig");
const OutputOption = @import("OutputOption.zig");
const log = std.log.scoped(.river_options);
pub const Value = union(enum) { pub const Value = union(enum) {
unset: void,
int: i32, int: i32,
uint: u32, uint: u32,
fixed: wl.Fixed, fixed: wl.Fixed,
string: ?[*:0]const u8, string: ?[*:0]const u8,
fn dupe(value: Value) !Value { pub fn dupe(value: Value) !Value {
return switch (value) { return switch (value) {
.string => |v| Value{ .string = if (v) |s| try util.gpa.dupeZ(u8, mem.span(s)) else null }, .string => |v| Value{ .string = if (v) |s| try util.gpa.dupeZ(u8, mem.span(s)) else null },
else => value, else => value,
}; };
} }
fn deinit(value: *Value) void { pub fn deinit(value: *Value) void {
if (value.* == .string) if (value.string) |s| util.gpa.free(mem.span(s)); if (value.* == .string) if (value.string) |s| util.gpa.free(mem.span(s));
} }
}; };
@ -52,19 +54,20 @@ pub const Value = union(enum) {
options_manager: *OptionsManager, options_manager: *OptionsManager,
link: wl.list.Link = undefined, link: wl.list.Link = undefined,
output: ?*Output, key: [:0]const u8,
key: [*:0]const u8,
value: Value, value: Value,
output_options: wl.list.Head(OutputOption, "link") = undefined,
event: struct { event: struct {
/// Emitted whenever the value of the option changes. /// Emitted whenever the value of the option changes.
update: wl.Signal(*Self), update: wl.Signal(*Value),
} = undefined, } = undefined,
handles: wl.list.Head(zriver.OptionHandleV1, null) = undefined, handles: wl.list.Head(river.OptionHandleV2, null) = undefined,
/// Allocate a new option, duping the provided key and value /// Allocate a new option, duping the provided key and value
pub fn create(options_manager: *OptionsManager, output: ?*Output, key: [*:0]const u8, value: Value) !*Self { pub fn create(options_manager: *OptionsManager, key: [*:0]const u8, value: Value) !void {
const self = try util.gpa.create(Self); const self = try util.gpa.create(Self);
errdefer util.gpa.destroy(self); errdefer util.gpa.destroy(self);
@ -73,56 +76,70 @@ pub fn create(options_manager: *OptionsManager, output: ?*Output, key: [*:0]cons
self.* = .{ self.* = .{
.options_manager = options_manager, .options_manager = options_manager,
.output = output,
.key = try util.gpa.dupeZ(u8, mem.span(key)), .key = try util.gpa.dupeZ(u8, mem.span(key)),
.value = owned_value, .value = owned_value,
}; };
self.handles.init(); errdefer util.gpa.free(self.key);
self.output_options.init();
errdefer {
var it = self.output_options.safeIterator(.forward);
while (it.next()) |output_option| output_option.destroy();
}
var it = options_manager.server.root.all_outputs.first;
while (it) |node| : (it = node.next) try OutputOption.create(self, node.data);
self.event.update.init(); self.event.update.init();
self.handles.init();
options_manager.options.append(self); options_manager.options.append(self);
return self;
} }
pub fn destroy(self: *Self) void { pub fn destroy(self: *Self) void {
{
var it = self.handles.safeIterator(.forward); var it = self.handles.safeIterator(.forward);
while (it.next()) |handle| handle.destroy(); while (it.next()) |handle| handle.destroy();
if (self.value == .string) if (self.value.string) |s| util.gpa.free(mem.span(s)); }
{
var it = self.output_options.safeIterator(.forward);
while (it.next()) |output_option| output_option.destroy();
}
self.value.deinit();
self.link.remove(); self.link.remove();
util.gpa.destroy(self); util.gpa.destroy(self);
} }
/// Asserts that the new value is not .unset. pub fn getOutputOption(self: *Self, output: *Output) ?*OutputOption {
/// Ignores the new value if the value is currently set and the type does not match. var it = self.output_options.iterator(.forward);
while (it.next()) |output_option| {
if (output_option.output == output) return output_option;
} else return null;
}
/// If the value is a string, the string is cloned. /// If the value is a string, the string is cloned.
/// If the value is changed, send the proper event to all clients /// If the value is changed, send the proper event to all clients
pub fn set(self: *Self, value: Value) !void { pub fn set(self: *Self, value: Value) !void {
std.debug.assert(value != .unset); if (meta.activeTag(value) != meta.activeTag(self.value)) return error.TypeMismatch;
if (self.value != .unset and meta.activeTag(value) != meta.activeTag(self.value)) return;
if (switch (self.value) {
.unset => true,
// TODO: std.mem needs a good way to compare optional sentinel pointers
.string => ((self.value.string == null) != (value.string == null)) or
(self.value.string != null and value.string != null and
std.cstr.cmp(self.value.string.?, value.string.?) != 0),
else => !std.meta.eql(self.value, value),
}) {
self.value.deinit(); self.value.deinit();
self.value = try value.dupe(); self.value = try value.dupe();
{
var it = self.handles.iterator(.forward); var it = self.handles.iterator(.forward);
while (it.next()) |handle| self.sendValue(handle); while (it.next()) |handle| self.sendValue(handle);
// Call listeners, if any.
self.event.update.emit(self);
} }
{
var it = self.output_options.iterator(.forward);
while (it.next()) |output_option| {
if (output_option.value == null) output_option.notifyChanged();
}
}
self.event.update.emit(&self.value);
} }
fn sendValue(self: Self, handle: *zriver.OptionHandleV1) void { pub fn sendValue(self: Self, handle: *river.OptionHandleV2) void {
switch (self.value) { switch (self.value) {
.unset => handle.sendUnset(),
.int => |v| handle.sendIntValue(v), .int => |v| handle.sendIntValue(v),
.uint => |v| handle.sendUintValue(v), .uint => |v| handle.sendUintValue(v),
.fixed => |v| handle.sendFixedValue(v), .fixed => |v| handle.sendFixedValue(v),
@ -130,24 +147,38 @@ fn sendValue(self: Self, handle: *zriver.OptionHandleV1) void {
} }
} }
pub fn addHandle(self: *Self, handle: *zriver.OptionHandleV1) void { pub fn addHandle(self: *Self, output: ?*Output, handle: *river.OptionHandleV2) void {
if (output) |o| {
self.getOutputOption(o).?.addHandle(handle);
} else {
self.handles.append(handle); self.handles.append(handle);
self.sendValue(handle); self.sendValue(handle);
handle.setHandler(*Self, handleRequest, handleDestroy, self); handle.setHandler(*Self, handleRequest, handleDestroy, self);
}
} }
fn handleRequest(handle: *zriver.OptionHandleV1, request: zriver.OptionHandleV1.Request, self: *Self) void { fn handleRequest(handle: *river.OptionHandleV2, request: river.OptionHandleV2.Request, self: *Self) void {
switch (request) { switch (request) {
.destroy => handle.destroy(), .destroy => handle.destroy(),
.set_int_value => |req| self.set(.{ .int = req.value }) catch unreachable, .set_int_value => |req| self.set(.{ .int = req.value }) catch |err| switch (err) {
.set_uint_value => |req| self.set(.{ .uint = req.value }) catch unreachable, error.TypeMismatch => handle.postError(.type_mismatch, "option is not of type int"),
.set_fixed_value => |req| self.set(.{ .fixed = req.value }) catch unreachable, error.OutOfMemory => unreachable,
.set_string_value => |req| self.set(.{ .string = req.value }) catch { },
handle.getClient().postNoMemory(); .set_uint_value => |req| self.set(.{ .uint = req.value }) catch |err| switch (err) {
error.TypeMismatch => handle.postError(.type_mismatch, "option is not of type uint"),
error.OutOfMemory => unreachable,
},
.set_fixed_value => |req| self.set(.{ .fixed = req.value }) catch |err| switch (err) {
error.TypeMismatch => handle.postError(.type_mismatch, "option is not of type fixed"),
error.OutOfMemory => unreachable,
},
.set_string_value => |req| self.set(.{ .string = req.value }) catch |err| switch (err) {
error.TypeMismatch => handle.postError(.type_mismatch, "option is not of type string"),
error.OutOfMemory => handle.getClient().postNoMemory(),
}, },
} }
} }
fn handleDestroy(handle: *zriver.OptionHandleV1, self: *Self) void { fn handleDestroy(handle: *river.OptionHandleV2, self: *Self) void {
handle.getLink().remove(); handle.getLink().remove();
} }

View file

@ -1,6 +1,6 @@
// This file is part of river, a dynamic tiling wayland compositor. // This file is part of river, a dynamic tiling wayland compositor.
// //
// Copyright 2020 The River Developers // Copyright 2020-2021 The River Developers
// //
// This program is free software: you can redistribute it and/or modify // 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 // it under the terms of the GNU General Public License as published by
@ -18,10 +18,11 @@
const Self = @This(); const Self = @This();
const std = @import("std"); const std = @import("std");
const mem = std.mem;
const wayland = @import("wayland"); const wayland = @import("wayland");
const wl = wayland.server.wl; const wl = wayland.server.wl;
const zriver = wayland.server.zriver; const river = wayland.server.river;
const wlr = @import("wlroots"); const wlr = @import("wlroots");
@ -29,8 +30,12 @@ const util = @import("util.zig");
const Option = @import("Option.zig"); const Option = @import("Option.zig");
const Output = @import("Output.zig"); const Output = @import("Output.zig");
const OutputOption = @import("OutputOption.zig");
const Server = @import("Server.zig"); const Server = @import("Server.zig");
const log = std.log.scoped(.river_options);
server: *Server,
global: *wl.Global, global: *wl.Global,
server_destroy: wl.Listener(*wl.Server) = wl.Listener(*wl.Server).init(handleServerDestroy), server_destroy: wl.Listener(*wl.Server) = wl.Listener(*wl.Server).init(handleServerDestroy),
@ -38,16 +43,25 @@ options: wl.list.Head(Option, "link") = undefined,
pub fn init(self: *Self, server: *Server) !void { pub fn init(self: *Self, server: *Server) !void {
self.* = .{ self.* = .{
.global = try wl.Global.create(server.wl_server, zriver.OptionsManagerV1, 1, *Self, self, bind), .server = server,
.global = try wl.Global.create(server.wl_server, river.OptionsManagerV2, 1, *Self, self, bind),
}; };
self.options.init(); self.options.init();
server.wl_server.addDestroyListener(&self.server_destroy); server.wl_server.addDestroyListener(&self.server_destroy);
try Option.create(self, "layout", .{ .string = null });
try Option.create(self, "output_title", .{ .string = null });
} }
pub fn handleOutputDestroy(self: *Self, output: *Output) void { pub fn createOutputOptions(self: *Self, output: *Output) !void {
var it = self.options.safeIterator(.forward); var it = self.options.iterator(.forward);
while (it.next()) |option| try OutputOption.create(option, output);
}
pub fn destroyOutputOptions(self: *Self, output: *Output) void {
var it = self.options.iterator(.forward);
while (it.next()) |option| { while (it.next()) |option| {
if (option.output == output) option.destroy(); if (option.getOutputOption(output)) |output_option| output_option.destroy();
} }
} }
@ -59,20 +73,53 @@ fn handleServerDestroy(listener: *wl.Listener(*wl.Server), wl_server: *wl.Server
} }
fn bind(client: *wl.Client, self: *Self, version: u32, id: u32) callconv(.C) void { fn bind(client: *wl.Client, self: *Self, version: u32, id: u32) callconv(.C) void {
const options_manager = zriver.OptionsManagerV1.create(client, 1, id) catch { const options_manager = river.OptionsManagerV2.create(client, version, id) catch {
client.postNoMemory(); client.postNoMemory();
return; return;
}; };
options_manager.setHandler(*Self, handleRequest, null, self); options_manager.setHandler(*Self, handleRequest, null, self);
} }
pub fn getOption(self: *Self, key: [:0]const u8) ?*Option {
var it = self.options.iterator(.forward);
while (it.next()) |option| {
if (mem.eql(u8, option.key, key)) return option;
} else return null;
}
fn handleRequest( fn handleRequest(
options_manager: *zriver.OptionsManagerV1, options_manager: *river.OptionsManagerV2,
request: zriver.OptionsManagerV1.Request, request: river.OptionsManagerV2.Request,
self: *Self, self: *Self,
) void { ) void {
switch (request) { switch (request) {
.destroy => options_manager.destroy(), .destroy => options_manager.destroy(),
.declare_int_option => |req| if (self.getOption(mem.span(req.key)) == null) {
Option.create(self, req.key, .{ .int = req.value }) catch {
options_manager.getClient().postNoMemory();
return;
};
},
.declare_uint_option => |req| if (self.getOption(mem.span(req.key)) == null) {
Option.create(self, req.key, .{ .uint = req.value }) catch {
options_manager.getClient().postNoMemory();
return;
};
},
.declare_string_option => |req| if (self.getOption(mem.span(req.key)) == null) {
Option.create(self, req.key, .{ .string = req.value }) catch {
options_manager.getClient().postNoMemory();
return;
};
},
.declare_fixed_option => |req| if (self.getOption(mem.span(req.key)) == null) {
Option.create(self, req.key, .{ .fixed = req.value }) catch {
options_manager.getClient().postNoMemory();
return;
};
},
.get_option_handle => |req| { .get_option_handle => |req| {
const output = if (req.output) |wl_output| blk: { const output = if (req.output) |wl_output| blk: {
// Ignore if the wl_output is inert // Ignore if the wl_output is inert
@ -80,19 +127,24 @@ fn handleRequest(
break :blk @intToPtr(*Output, wlr_output.data); break :blk @intToPtr(*Output, wlr_output.data);
} else null; } else null;
// Look for an existing Option, if not found create a new one const option = self.getOption(mem.span(req.key)) orelse {
var it = self.options.iterator(.forward); // There is no option with the requested key. In this case
const option = while (it.next()) |option| { // all we do is send an undeclared event and wait for the
if (option.output == output and std.cstr.cmp(option.key, req.key) == 0) { // client to destroy the resource.
break option; const handle = river.OptionHandleV2.create(
} options_manager.getClient(),
} else options_manager.getVersion(),
Option.create(self, output, req.key, .unset) catch { req.handle,
) catch {
options_manager.getClient().postNoMemory(); options_manager.getClient().postNoMemory();
return; return;
}; };
handle.sendUndeclared();
handle.setHandler(*Self, undeclaredHandleRequest, null, self);
return;
};
const handle = zriver.OptionHandleV1.create( const handle = river.OptionHandleV2.create(
options_manager.getClient(), options_manager.getClient(),
options_manager.getVersion(), options_manager.getVersion(),
req.handle, req.handle,
@ -101,7 +153,36 @@ fn handleRequest(
return; return;
}; };
option.addHandle(handle); option.addHandle(output, handle);
},
.unset_option => |req| {
// Ignore if the wl_output is inert
const wlr_output = wlr.Output.fromWlOutput(req.output) orelse return;
const output = @intToPtr(*Output, wlr_output.data);
const option = self.getOption(mem.span(req.key)) orelse return;
option.getOutputOption(output).?.unset();
},
}
}
fn undeclaredHandleRequest(
handle: *river.OptionHandleV2,
request: river.OptionHandleV2.Request,
self: *Self,
) void {
switch (request) {
.destroy => handle.destroy(),
.set_int_value,
.set_uint_value,
.set_fixed_value,
.set_string_value,
=> {
handle.postError(
.request_while_undeclared,
"a request other than destroy was made on a handle to an undeclared option",
);
}, },
} }
} }

View file

@ -39,6 +39,7 @@ const ViewStack = @import("view_stack.zig").ViewStack;
const AttachMode = @import("view_stack.zig").AttachMode; const AttachMode = @import("view_stack.zig").AttachMode;
const OutputStatus = @import("OutputStatus.zig"); const OutputStatus = @import("OutputStatus.zig");
const Option = @import("Option.zig"); const Option = @import("Option.zig");
const OutputOption = @import("OutputOption.zig");
const State = struct { const State = struct {
/// A bit field of focused tags /// A bit field of focused tags
@ -94,11 +95,10 @@ enable: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleEnable),
frame: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleFrame), frame: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleFrame),
mode: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleMode), mode: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleMode),
layout_option: *Option, layout_option: *OutputOption,
/// Listeners for options output_title: wl.Listener(*Option.Value) = wl.Listener(*Option.Value).init(handleTitleChange),
output_title: wl.Listener(*Option) = wl.Listener(*Option).init(handleTitleChange), layout_change: wl.Listener(*Option.Value) = wl.Listener(*Option.Value).init(handleLayoutChange),
layout_change: wl.Listener(*Option) = wl.Listener(*Option).init(handleLayoutChange),
pub fn init(self: *Self, root: *Root, wlr_output: *wlr.Output) !void { pub fn init(self: *Self, root: *Root, wlr_output: *wlr.Output) !void {
// Some backends don't have modes. DRM+KMS does, and we need to set a mode // Some backends don't have modes. DRM+KMS does, and we need to set a mode
@ -150,24 +150,29 @@ pub fn init(self: *Self, root: *Root, wlr_output: *wlr.Output) !void {
.width = effective_resolution.width, .width = effective_resolution.width,
.height = effective_resolution.height, .height = effective_resolution.height,
}; };
}
const options_manager = &root.server.options_manager;
try options_manager.createOutputOptions(self);
errdefer options_manager.destroyOutputOptions(self);
// Set the default title of this output // Set the default title of this output
var buf: ["river - ".len + wlr_output.name.len + 1]u8 = undefined; var buf: ["river - ".len + wlr_output.name.len + 1]u8 = undefined;
const default_title = fmt.bufPrintZ(&buf, "river - {}", .{mem.spanZ(&wlr_output.name)}) catch unreachable; const default_title = fmt.bufPrintZ(&buf, "river - {}", .{mem.spanZ(&wlr_output.name)}) catch unreachable;
self.setTitle(default_title); self.setTitle(default_title);
// Create all default output options const global_title_option = options_manager.getOption("output_title") orelse unreachable;
const options_manager = &root.server.options_manager; const title_option = global_title_option.getOutputOption(self).?;
self.layout_option = try Option.create(options_manager, self, "layout", .{ .string = null }); title_option.set(.{ .string = default_title }) catch |err| switch (err) {
const title_option = try Option.create(options_manager, self, "output_title", .{ .string = default_title.ptr }); error.TypeMismatch => unreachable,
_ = try Option.create(options_manager, self, "main_amount", .{ .uint = 1 }); error.OutOfMemory => return err,
_ = try Option.create(options_manager, self, "main_factor", .{ .fixed = wl.Fixed.fromDouble(0.6) }); };
_ = try Option.create(options_manager, self, "view_padding", .{ .uint = 10 });
_ = try Option.create(options_manager, self, "outer_padding", .{ .uint = 10 }); const global_layout_option = options_manager.getOption("layout") orelse unreachable;
self.layout_option = global_layout_option.getOutputOption(self).?;
self.layout_option.event.update.add(&self.layout_change); self.layout_option.event.update.add(&self.layout_change);
title_option.event.update.add(&self.output_title); title_option.event.update.add(&self.output_title);
}
} }
pub fn getLayer(self: *Self, layer: zwlr.LayerShellV1.Layer) *std.TailQueue(LayerSurface) { pub fn getLayer(self: *Self, layer: zwlr.LayerShellV1.Layer) *std.TailQueue(LayerSurface) {
@ -440,7 +445,7 @@ fn handleDestroy(listener: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) v
std.log.scoped(.server).debug("output '{}' destroyed", .{self.wlr_output.name}); std.log.scoped(.server).debug("output '{}' destroyed", .{self.wlr_output.name});
root.server.options_manager.handleOutputDestroy(self); root.server.options_manager.destroyOutputOptions(self);
// Remove the destroyed output from root if it wasn't already removed // Remove the destroyed output from root if it wasn't already removed
root.removeOutput(self); root.removeOutput(self);
@ -509,20 +514,21 @@ pub fn setTitle(self: *Self, title: [*:0]const u8) void {
} }
} }
fn handleTitleChange(listener: *wl.Listener(*Option), option: *Option) void { fn handleTitleChange(listener: *wl.Listener(*Option.Value), value: *Option.Value) void {
if (option.value.string) |title| option.output.?.setTitle(title); const self = @fieldParentPtr(Self, "output_title", listener);
if (value.string) |title| self.setTitle(title);
} }
fn handleLayoutChange(listener: *wl.Listener(*Option), option: *Option) void { fn handleLayoutChange(listener: *wl.Listener(*Option.Value), value: *Option.Value) void {
const self = @fieldParentPtr(Self, "layout_change", listener);
// The user changed the layout namespace of this output. Try to find a // The user changed the layout namespace of this output. Try to find a
// matching layout. // matching layout.
const output = option.output.?; self.pending.layout = if (value.string) |namespace| blk: {
output.pending.layout = if (option.value.string) |namespace| blk: { var layout_it = self.layouts.first;
var layout_it = output.layouts.first;
break :blk while (layout_it) |node| : (layout_it = node.next) { break :blk while (layout_it) |node| : (layout_it = node.next) {
if (mem.eql(u8, mem.span(namespace), node.data.namespace)) break &node.data; if (mem.eql(u8, mem.span(namespace), node.data.namespace)) break &node.data;
} else null; } else null;
} else null; } else null;
output.arrangeViews(); self.arrangeViews();
output.root.startTransaction(); self.root.startTransaction();
} }

145
river/OutputOption.zig Normal file
View file

@ -0,0 +1,145 @@
// This file is part of river, a dynamic tiling wayland compositor.
//
// Copyright 2021 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 <https://www.gnu.org/licenses/>.
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 river = wayland.server.river;
const util = @import("util.zig");
const Output = @import("Output.zig");
const OptionsManager = @import("OptionsManager.zig");
const Option = @import("Option.zig");
const Value = Option.Value;
option: *Option,
link: wl.list.Link = undefined,
output: *Output,
value: ?Value = null,
event: struct {
/// Emitted whenever the value of the option changes.
update: wl.Signal(*Value),
} = undefined,
handles: wl.list.Head(river.OptionHandleV2, null) = undefined,
pub fn create(option: *Option, output: *Output) !void {
const self = try util.gpa.create(Self);
errdefer util.gpa.destroy(self);
self.* = .{ .option = option, .output = output };
self.event.update.init();
self.handles.init();
option.output_options.append(self);
}
pub fn destroy(self: *Self) void {
if (self.value) |*value| value.deinit();
self.link.remove();
util.gpa.destroy(self);
}
pub fn addHandle(self: *Self, handle: *river.OptionHandleV2) void {
self.handles.append(handle);
self.sendValue(handle);
handle.setHandler(*Self, handleRequest, handleDestroy, self);
}
pub fn unset(self: *Self) void {
if (self.value) |*value| value.deinit();
self.value = null;
// Unsetting the output-specific value causes us to fall back to the
// global value. Send this new value to all clients.
var it = self.handles.iterator(.forward);
while (it.next()) |handle| {
self.option.sendValue(handle);
}
self.event.update.emit(&self.option.value);
}
/// 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 {
if (meta.activeTag(value) != meta.activeTag(self.option.value)) return error.TypeMismatch;
if (self.value) |*v| v.deinit();
self.value = try value.dupe();
self.notifyChanged();
}
pub fn notifyChanged(self: *Self) void {
var it = self.handles.iterator(.forward);
while (it.next()) |handle| self.sendValue(handle);
self.event.update.emit(self.get());
}
pub fn get(self: *Self) *Value {
return if (self.value) |*value| value else &self.option.value;
}
fn sendValue(self: Self, handle: *river.OptionHandleV2) void {
if (self.value) |value| {
switch (value) {
.int => |v| handle.sendIntValue(v),
.uint => |v| handle.sendUintValue(v),
.fixed => |v| handle.sendFixedValue(v),
.string => |v| handle.sendStringValue(v),
}
} else {
self.option.sendValue(handle);
}
}
fn handleRequest(handle: *river.OptionHandleV2, request: river.OptionHandleV2.Request, self: *Self) void {
switch (request) {
.destroy => handle.destroy(),
.set_int_value => |req| self.set(.{ .int = req.value }) catch |err| switch (err) {
error.TypeMismatch => handle.postError(.type_mismatch, "option is not of type int"),
error.OutOfMemory => unreachable,
},
.set_uint_value => |req| self.set(.{ .uint = req.value }) catch |err| switch (err) {
error.TypeMismatch => handle.postError(.type_mismatch, "option is not of type uint"),
error.OutOfMemory => unreachable,
},
.set_fixed_value => |req| self.set(.{ .fixed = req.value }) catch |err| switch (err) {
error.TypeMismatch => handle.postError(.type_mismatch, "option is not of type fixed"),
error.OutOfMemory => unreachable,
},
.set_string_value => |req| self.set(.{ .string = req.value }) catch |err| switch (err) {
error.TypeMismatch => handle.postError(.type_mismatch, "option is not of type string"),
error.OutOfMemory => handle.getClient().postNoMemory(),
},
}
}
fn handleDestroy(handle: *river.OptionHandleV2, self: *Self) void {
handle.getLink().remove();
}

View file

@ -115,9 +115,9 @@ pub fn init(self: *Self) !void {
self.config = try Config.init(); self.config = try Config.init();
try self.decoration_manager.init(self); try self.decoration_manager.init(self);
try self.options_manager.init(self);
try self.root.init(self); try self.root.init(self);
// Must be called after root is initialized // Must be called after root is initialized
try self.options_manager.init(self);
try self.input_manager.init(self); try self.input_manager.init(self);
try self.control.init(self); try self.control.init(self);
try self.status_manager.init(self); try self.status_manager.init(self);

View file

@ -22,6 +22,7 @@ const assert = std.debug.assert;
const wayland = @import("wayland"); const wayland = @import("wayland");
const wl = wayland.client.wl; const wl = wayland.client.wl;
const river = wayland.client.river;
const zriver = wayland.client.zriver; const zriver = wayland.client.zriver;
const zxdg = wayland.client.zxdg; const zxdg = wayland.client.zxdg;
@ -36,7 +37,7 @@ pub const Output = struct {
pub const Globals = struct { pub const Globals = struct {
control: ?*zriver.ControlV1 = null, control: ?*zriver.ControlV1 = null,
options_manager: ?*zriver.OptionsManagerV1 = null, options_manager: ?*river.OptionsManagerV2 = null,
status_manager: ?*zriver.StatusManagerV1 = null, status_manager: ?*zriver.StatusManagerV1 = null,
seat: ?*wl.Seat = null, seat: ?*wl.Seat = null,
output_manager: ?*zxdg.OutputManagerV1 = null, output_manager: ?*zxdg.OutputManagerV1 = null,
@ -87,6 +88,8 @@ fn _main() !void {
try options.getOption(display, &globals); try options.getOption(display, &globals);
} else if (os.argv.len > 2 and mem.eql(u8, "set-option", mem.span(os.argv[1]))) { } else if (os.argv.len > 2 and mem.eql(u8, "set-option", mem.span(os.argv[1]))) {
try options.setOption(display, &globals); try options.setOption(display, &globals);
} else if (os.argv.len > 2 and mem.eql(u8, "unset-option", mem.span(os.argv[1]))) {
try options.unsetOption(display, &globals);
} else if (os.argv.len > 2 and mem.eql(u8, "mod-option", mem.span(os.argv[1]))) { } else if (os.argv.len > 2 and mem.eql(u8, "mod-option", mem.span(os.argv[1]))) {
try options.modOption(display, &globals); try options.modOption(display, &globals);
} else { } else {
@ -115,8 +118,8 @@ fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, globals: *
globals.seat = registry.bind(global.name, wl.Seat, 1) catch @panic("out of memory"); globals.seat = registry.bind(global.name, wl.Seat, 1) catch @panic("out of memory");
} else if (std.cstr.cmp(global.interface, zriver.ControlV1.getInterface().name) == 0) { } else if (std.cstr.cmp(global.interface, zriver.ControlV1.getInterface().name) == 0) {
globals.control = registry.bind(global.name, zriver.ControlV1, 1) catch @panic("out of memory"); globals.control = registry.bind(global.name, zriver.ControlV1, 1) catch @panic("out of memory");
} else if (std.cstr.cmp(global.interface, zriver.OptionsManagerV1.getInterface().name) == 0) { } else if (std.cstr.cmp(global.interface, river.OptionsManagerV2.getInterface().name) == 0) {
globals.options_manager = registry.bind(global.name, zriver.OptionsManagerV1, 1) catch @panic("out of memory"); globals.options_manager = registry.bind(global.name, river.OptionsManagerV2, 1) catch @panic("out of memory");
} else if (std.cstr.cmp(global.interface, zriver.StatusManagerV1.getInterface().name) == 0) { } else if (std.cstr.cmp(global.interface, zriver.StatusManagerV1.getInterface().name) == 0) {
globals.status_manager = registry.bind(global.name, zriver.StatusManagerV1, 1) catch @panic("out of memory"); globals.status_manager = registry.bind(global.name, zriver.StatusManagerV1, 1) catch @panic("out of memory");
} else if (std.cstr.cmp(global.interface, zxdg.OutputManagerV1.getInterface().name) == 0 and global.version >= 2) { } else if (std.cstr.cmp(global.interface, zxdg.OutputManagerV1.getInterface().name) == 0 and global.version >= 2) {

View file

@ -17,11 +17,13 @@
const std = @import("std"); const std = @import("std");
const os = std.os; const os = std.os;
const math = std.math;
const mem = std.mem; const mem = std.mem;
const fmt = std.fmt; const fmt = std.fmt;
const wayland = @import("wayland"); const wayland = @import("wayland");
const wl = wayland.client.wl; const wl = wayland.client.wl;
const river = wayland.client.river;
const zriver = wayland.client.zriver; const zriver = wayland.client.zriver;
const zxdg = wayland.client.zxdg; const zxdg = wayland.client.zxdg;
@ -49,69 +51,42 @@ const Context = struct {
pub fn declareOption(display: *wl.Display, globals: *Globals) !void { pub fn declareOption(display: *wl.Display, globals: *Globals) !void {
// https://github.com/ziglang/zig/issues/7807 // https://github.com/ziglang/zig/issues/7807
const argv: [][*:0]const u8 = os.argv; const argv: [][*:0]const u8 = os.argv;
const args = Args(3, &[_]FlagDef{ const args = Args(3, &[_]FlagDef{}).parse(argv[2..]);
.{ .name = "-output", .kind = .arg },
.{ .name = "-focused-output", .kind = .boolean },
}).parse(argv[2..]);
const key = args.positionals[0]; const key = args.positionals[0];
const value_type = std.meta.stringToEnum(ValueType, mem.span(args.positionals[1])) orelse const value_type = std.meta.stringToEnum(ValueType, mem.span(args.positionals[1])) orelse {
root.printErrorExit("'{}' is not a valid type, must be int, uint, fixed, or string", .{args.positionals[1]}); root.printErrorExit(
"'{}' is not a valid type, must be int, uint, fixed, or string",
.{args.positionals[1]},
);
};
const raw_value = args.positionals[2]; const raw_value = args.positionals[2];
const output = if (args.argFlag("-output")) |o|
try parseOutputName(display, globals, o)
else if (args.boolFlag("-focused-output"))
try getFocusedOutput(display, globals)
else
null;
const options_manager = globals.options_manager orelse return error.RiverOptionsManagerNotAdvertised; const options_manager = globals.options_manager orelse return error.RiverOptionsManagerNotAdvertised;
const handle = try options_manager.getOptionHandle(key, if (output) |o| o.wl_output else null);
switch (value_type) { switch (value_type) {
.int => setIntValueRaw(handle, raw_value), .int => options_manager.declareIntOption(key, parseInt(raw_value)),
.uint => setUintValueRaw(handle, raw_value), .uint => options_manager.declareUintOption(key, parseUint(raw_value)),
.fixed => setFixedValueRaw(handle, raw_value), .fixed => options_manager.declareFixedOption(key, parseFixed(raw_value)),
.string => handle.setStringValue(if (raw_value[0] == 0) null else raw_value), .string => options_manager.declareStringOption(key, raw_value),
} }
_ = display.flush() catch os.exit(1);
_ = try display.flush();
} }
fn setIntValueRaw(handle: *zriver.OptionHandleV1, raw_value: [*:0]const u8) void { fn parseInt(raw_value: [*:0]const u8) i32 {
handle.setIntValue(fmt.parseInt(i32, mem.span(raw_value), 10) catch return fmt.parseInt(i32, mem.span(raw_value), 10) catch
root.printErrorExit("{} is not a valid int", .{raw_value})); root.printErrorExit("{} is not a valid int", .{raw_value});
} }
fn setUintValueRaw(handle: *zriver.OptionHandleV1, raw_value: [*:0]const u8) void { fn parseUint(raw_value: [*:0]const u8) u32 {
handle.setUintValue(fmt.parseInt(u32, mem.span(raw_value), 10) catch return fmt.parseInt(u32, mem.span(raw_value), 10) catch
root.printErrorExit("{} is not a valid uint", .{raw_value})); root.printErrorExit("{} is not a valid uint", .{raw_value});
} }
fn setFixedValueRaw(handle: *zriver.OptionHandleV1, raw_value: [*:0]const u8) void { fn parseFixed(raw_value: [*:0]const u8) wl.Fixed {
handle.setFixedValue(wl.Fixed.fromDouble(fmt.parseFloat(f64, mem.span(raw_value)) catch return wl.Fixed.fromDouble(fmt.parseFloat(f64, mem.span(raw_value)) catch
root.printErrorExit("{} is not a valid fixed", .{raw_value}))); root.printErrorExit("{} is not a valid fixed", .{raw_value}));
}
fn modIntValueRaw(handle: *zriver.OptionHandleV1, current: i32, raw_value: [*:0]const u8) void {
const mod = fmt.parseInt(i32, mem.span(raw_value), 10) catch
root.printErrorExit("{} is not a valid int modificator", .{raw_value});
handle.setIntValue(current + mod);
}
fn modUintValueRaw(handle: *zriver.OptionHandleV1, current: u32, raw_value: [*:0]const u8) void {
// We need to allow negative mod values, but the value of the option may
// never be below zero.
const mod = fmt.parseInt(i32, mem.span(raw_value), 10) catch
root.printErrorExit("{} is not a valid uint modificator", .{raw_value});
const new = @intCast(i32, current) + mod;
handle.setUintValue(if (new < 0) 0 else @intCast(u32, new));
}
fn modFixedValueRaw(handle: *zriver.OptionHandleV1, current: wl.Fixed, raw_value: [*:0]const u8) void {
const mod = fmt.parseFloat(f64, mem.span(raw_value)) catch
root.printErrorExit("{} is not a valid fixed modificator", .{raw_value});
handle.setFixedValue(wl.Fixed.fromDouble(current.toDouble() + mod));
} }
pub fn getOption(display: *wl.Display, globals: *Globals) !void { pub fn getOption(display: *wl.Display, globals: *Globals) !void {
@ -174,6 +149,30 @@ pub fn setOption(display: *wl.Display, globals: *Globals) !void {
while (true) _ = try display.dispatch(); while (true) _ = try display.dispatch();
} }
pub fn unsetOption(display: *wl.Display, globals: *Globals) !void {
// https://github.com/ziglang/zig/issues/7807
const argv: [][*:0]const u8 = os.argv;
const args = Args(1, &[_]FlagDef{
.{ .name = "-output", .kind = .arg },
.{ .name = "-focused-output", .kind = .boolean },
}).parse(argv[2..]);
const output = if (args.argFlag("-output")) |o|
try parseOutputName(display, globals, o)
else if (args.boolFlag("-focused-output"))
try getFocusedOutput(display, globals)
else
root.printErrorExit("unset requires either -output or -focused-output", .{});
const key = args.positionals[0];
const options_manager = globals.options_manager orelse return error.RiverOptionsManagerNotAdvertised;
options_manager.unsetOption(key, output.wl_output);
_ = try display.flush();
}
pub fn modOption(display: *wl.Display, globals: *Globals) !void { pub fn modOption(display: *wl.Display, globals: *Globals) !void {
// https://github.com/ziglang/zig/issues/7807 // https://github.com/ziglang/zig/issues/7807
const argv: [][*:0]const u8 = os.argv; const argv: [][*:0]const u8 = os.argv;
@ -246,16 +245,12 @@ fn seatStatusListener(seat_status: *zriver.SeatStatusV1, event: zriver.SeatStatu
} }
fn getOptionListener( fn getOptionListener(
handle: *zriver.OptionHandleV1, handle: *river.OptionHandleV2,
event: zriver.OptionHandleV1.Event, event: river.OptionHandleV2.Event,
ctx: *const Context, ctx: *const Context,
) void { ) void {
switch (event) { switch (event) {
.unset => if (ctx.output) |output| { .undeclared => root.printErrorExit("option '{}' has not been declared", .{ctx.key}),
root.printErrorExit("option '{}' has not been declared on output '{}'", .{ ctx.key, output.name });
} else {
root.printErrorExit("option '{}' has not been declared globally", .{ctx.key});
},
.int_value => |ev| printOutputExit("{}", .{ev.value}), .int_value => |ev| printOutputExit("{}", .{ev.value}),
.uint_value => |ev| printOutputExit("{}", .{ev.value}), .uint_value => |ev| printOutputExit("{}", .{ev.value}),
.fixed_value => |ev| printOutputExit("{d}", .{ev.value.toDouble()}), .fixed_value => |ev| printOutputExit("{d}", .{ev.value.toDouble()}),
@ -270,19 +265,15 @@ fn printOutputExit(comptime format: []const u8, args: anytype) noreturn {
} }
fn setOptionListener( fn setOptionListener(
handle: *zriver.OptionHandleV1, handle: *river.OptionHandleV2,
event: zriver.OptionHandleV1.Event, event: river.OptionHandleV2.Event,
ctx: *const Context, ctx: *const Context,
) void { ) void {
switch (event) { switch (event) {
.unset => if (ctx.output) |output| { .undeclared => root.printErrorExit("option '{}' has not been declared", .{ctx.key}),
root.printErrorExit("option '{}' has not been declared on output '{}'", .{ ctx.key, output.name }); .int_value => |ev| handle.setIntValue(parseInt(ctx.raw_value)),
} else { .uint_value => |ev| handle.setUintValue(parseUint(ctx.raw_value)),
root.printErrorExit("option '{}' has not been declared globally", .{ctx.key}); .fixed_value => |ev| handle.setFixedValue(parseFixed(ctx.raw_value)),
},
.int_value => |ev| setIntValueRaw(handle, ctx.raw_value),
.uint_value => |ev| setUintValueRaw(handle, ctx.raw_value),
.fixed_value => |ev| setFixedValueRaw(handle, ctx.raw_value),
.string_value => |ev| handle.setStringValue(if (ctx.raw_value[0] == 0) null else ctx.raw_value), .string_value => |ev| handle.setStringValue(if (ctx.raw_value[0] == 0) null else ctx.raw_value),
} }
_ = ctx.display.flush() catch os.exit(1); _ = ctx.display.flush() catch os.exit(1);
@ -290,16 +281,12 @@ fn setOptionListener(
} }
fn modOptionListener( fn modOptionListener(
handle: *zriver.OptionHandleV1, handle: *river.OptionHandleV2,
event: zriver.OptionHandleV1.Event, event: river.OptionHandleV2.Event,
ctx: *const Context, ctx: *const Context,
) void { ) void {
switch (event) { switch (event) {
.unset => if (ctx.output) |output| { .undeclared => root.printErrorExit("option '{}' has not been declared", .{ctx.key}),
root.printErrorExit("option '{}' has not been declared on output '{}'", .{ ctx.key, output.name });
} else {
root.printErrorExit("option '{}' has not been declared globally", .{ctx.key});
},
.int_value => |ev| modIntValueRaw(handle, ev.value, ctx.raw_value), .int_value => |ev| modIntValueRaw(handle, ev.value, ctx.raw_value),
.uint_value => |ev| modUintValueRaw(handle, ev.value, ctx.raw_value), .uint_value => |ev| modUintValueRaw(handle, ev.value, ctx.raw_value),
.fixed_value => |ev| modFixedValueRaw(handle, ev.value, ctx.raw_value), .fixed_value => |ev| modFixedValueRaw(handle, ev.value, ctx.raw_value),
@ -308,3 +295,26 @@ fn modOptionListener(
_ = ctx.display.flush() catch os.exit(1); _ = ctx.display.flush() catch os.exit(1);
os.exit(0); os.exit(0);
} }
fn modIntValueRaw(handle: *river.OptionHandleV2, current: i32, raw_value: [*:0]const u8) void {
const mod = fmt.parseInt(i32, mem.span(raw_value), 10) catch
root.printErrorExit("{} is not a valid int modifier", .{raw_value});
const new_value = math.add(i32, current, mod) catch
root.printErrorExit("provided value of {d} would overflow option if added", .{mod});
handle.setIntValue(new_value);
}
fn modUintValueRaw(handle: *river.OptionHandleV2, current: u32, raw_value: [*:0]const u8) void {
// We need to allow negative mod values, but the value of the option may
// never be below zero.
const mod = fmt.parseInt(i32, mem.span(raw_value), 10) catch
root.printErrorExit("{} is not a valid uint modifier", .{raw_value});
const new = @intCast(i32, current) + mod;
handle.setUintValue(if (new < 0) 0 else @intCast(u32, new));
}
fn modFixedValueRaw(handle: *river.OptionHandleV2, current: wl.Fixed, raw_value: [*:0]const u8) void {
const mod = fmt.parseFloat(f64, mem.span(raw_value)) catch
root.printErrorExit("{} is not a valid fixed modifier", .{raw_value});
handle.setFixedValue(wl.Fixed.fromDouble(current.toDouble() + mod));
}

View file

@ -1,6 +1,6 @@
// This file is part of river, a dynamic tiling wayland compositor. // This file is part of river, a dynamic tiling wayland compositor.
// //
// Copyright 2020 The River Developers // Copyright 2020-2021 The River Developers
// //
// This program is free software: you can redistribute it and/or modify // 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 // it under the terms of the GNU General Public License as published by
@ -14,15 +14,13 @@
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
//
//
// This is an implementation of the default "tiled" layout of dwm and the // This is an implementation of the default "tiled" layout of dwm and the
// 3 other orientations thereof. This code is written with the left // 3 other orientations thereof. This code is written for the main stack
// orientation in mind and then the input/output values are adjusted to apply // to the left and then the input/output values are adjusted to apply
// the necessary transformations to derive the other 3. // the necessary transformations to derive the other orientations.
// //
// With 4 views and one main, the left layout looks something like this: // With 4 views and one main on the left, the layout looks something like this:
// //
// +-----------------------+------------+ // +-----------------------+------------+
// | | | // | | |
@ -37,229 +35,164 @@
// | | | // | | |
// | | | // | | |
// +-----------------------+------------+ // +-----------------------+------------+
//
const std = @import("std"); const std = @import("std");
const mem = std.mem;
const assert = std.debug.assert;
const wayland = @import("wayland"); const wayland = @import("wayland");
const wl = wayland.client.wl; const wl = wayland.client.wl;
const zriver = wayland.client.zriver;
const river = wayland.client.river; const river = wayland.client.river;
const gpa = std.heap.c_allocator; const Location = enum {
const Context = struct {
running: bool = true,
layout_manager: ?*river.LayoutManagerV1 = null,
options_manager: ?*zriver.OptionsManagerV1 = null,
outputs: std.TailQueue(Output) = .{},
pub fn addOutput(self: *Context, registry: *wl.Registry, name: u32) !void {
const output = try registry.bind(name, wl.Output, 3);
const node = try gpa.create(std.TailQueue(Output).Node);
node.data.init(self, output);
self.outputs.append(node);
}
pub fn destroyAllOutputs(self: *Context) void {
while (self.outputs.pop()) |node| {
node.data.deinit();
gpa.destroy(node);
}
}
pub fn configureAllOutputs(self: *Context) void {
var it = self.outputs.first;
while (it) |node| : (it = node.next) {
node.data.configure(self);
}
}
};
const Option = struct {
pub const Value = union(enum) {
unset: void,
double: f64,
uint: u32,
};
handle: ?*zriver.OptionHandleV1 = null,
value: Value = .unset,
output: *Output = undefined,
pub fn init(self: *Option, output: *Output, comptime key: [*:0]const u8, initial: Value) !void {
self.* = .{
.value = initial,
.output = output,
.handle = try output.context.options_manager.?.getOptionHandle(
key,
output.output,
),
};
self.handle.?.setListener(*Option, optionListener, self) catch |err| {
self.handle.?.destroy();
self.handle = null;
return err;
};
}
pub fn deinit(self: *Option) void {
if (self.handle) |handle| handle.destroy();
}
fn optionListener(handle: *zriver.OptionHandleV1, event: zriver.OptionHandleV1.Event, self: *Option) void {
switch (event) {
.unset => switch (self.value) {
.uint => handle.setUintValue(self.value.uint),
.double => handle.setFixedValue(wl.Fixed.fromDouble(self.value.double)),
else => unreachable,
},
.int_value => {},
.uint_value => |data| self.value = .{ .uint = data.value },
.fixed_value => |data| self.value = .{ .double = data.value.toDouble() },
.string_value => {},
}
if (self.output.top.layout) |layout| layout.parametersChanged();
if (self.output.right.layout) |layout| layout.parametersChanged();
if (self.output.bottom.layout) |layout| layout.parametersChanged();
if (self.output.left.layout) |layout| layout.parametersChanged();
}
pub fn getValueOrElse(self: *Option, comptime T: type, comptime otherwise: T) T {
switch (T) {
u32 => return if (self.value == .uint) self.value.uint else otherwise,
f64 => return if (self.value == .double) self.value.double else otherwise,
else => @compileError("Unsupported type for Option.getValueOrElse()"),
}
}
};
const Output = struct {
context: *Context,
output: *wl.Output,
top: Layout = undefined,
right: Layout = undefined,
bottom: Layout = undefined,
left: Layout = undefined,
main_amount: Option = .{},
main_factor: Option = .{},
view_padding: Option = .{},
outer_padding: Option = .{},
configured: bool = false,
pub fn init(self: *Output, context: *Context, wl_output: *wl.Output) void {
self.* = .{
.output = wl_output,
.context = context,
};
self.configure(context);
}
pub fn deinit(self: *Output) void {
self.output.release();
if (self.configured) {
self.top.deinit();
self.right.deinit();
self.bottom.deinit();
self.left.deinit();
self.main_amount.deinit();
self.main_factor.deinit();
self.view_padding.deinit();
self.outer_padding.deinit();
}
}
pub fn configure(self: *Output, context: *Context) void {
if (self.configured) return;
if (context.layout_manager == null) return;
if (context.options_manager == null) return;
self.configured = true;
self.main_amount.init(self, "main_amount", .{ .uint = 1 }) catch {};
self.main_factor.init(self, "main_factor", .{ .double = 0.6 }) catch {};
self.view_padding.init(self, "view_padding", .{ .uint = 10 }) catch {};
self.outer_padding.init(self, "outer_padding", .{ .uint = 10 }) catch {};
self.top.init(self, .top) catch {};
self.right.init(self, .right) catch {};
self.bottom.init(self, .bottom) catch {};
self.left.init(self, .left) catch {};
}
};
const Layout = struct {
output: *Output,
layout: ?*river.LayoutV1,
orientation: Orientation,
const Orientation = enum {
top, top,
right, right,
bottom, bottom,
left, left,
};
const default_main_location: Location = .left;
const default_main_count = 1;
const default_main_factor = 0.6;
const default_view_padding = 6;
const default_outer_padding = 6;
/// We don't free resources on exit, only when output globals are removed.
const gpa = std.heap.c_allocator;
const Context = struct {
initialized: bool = false,
layout_manager: ?*river.LayoutManagerV1 = null,
options_manager: ?*river.OptionsManagerV2 = null,
outputs: std.TailQueue(Output) = .{},
fn addOutput(context: *Context, registry: *wl.Registry, name: u32) !void {
const wl_output = try registry.bind(name, wl.Output, 3);
errdefer wl_output.release();
const node = try gpa.create(std.TailQueue(Output).Node);
errdefer gpa.destroy(node);
try node.data.init(context, wl_output, name);
context.outputs.append(node);
}
};
fn Option(comptime key: [:0]const u8, comptime T: type, comptime default: T) type {
return struct {
const Self = @This();
output: *Output,
handle: *river.OptionHandleV2,
value: T = default,
fn init(option: *Self, context: *Context, output: *Output) !void {
option.* = .{
.output = output,
.handle = try context.options_manager.?.getOptionHandle(key, output.wl_output),
}; };
option.handle.setListener(*Self, optionListener, option) catch unreachable;
pub fn init(self: *Layout, output: *Output, orientation: Orientation) !void {
self.output = output;
self.orientation = orientation;
self.layout = try output.context.layout_manager.?.getLayout(
self.output.output,
self.getNamespace(),
);
self.layout.?.setListener(*Layout, layoutListener, self) catch |err| {
self.layout.?.destroy();
self.layout = null;
return err;
};
} }
fn getNamespace(self: *Layout) [*:0]const u8 { fn deinit(option: *Self) void {
return switch (self.orientation) { option.handle.destroy();
.top => "tile-top", option.* = undefined;
.right => "tile-right",
.bottom => "tile-bottom",
.left => "tile-left",
};
} }
pub fn deinit(self: *Layout) void { fn optionListener(handle: *river.OptionHandleV2, event: river.OptionHandleV2.Event, option: *Self) void {
if (self.layout) |layout| { const prev_value = option.value;
layout.destroy(); assert(event != .undeclared); // We declare all options used in main()
self.layout = null; switch (T) {
} u32 => switch (event) {
} .uint_value => |ev| option.value = ev.value,
else => std.log.err("expected value of uint type for " ++ key ++
fn layoutListener(layout: *river.LayoutV1, event: river.LayoutV1.Event, self: *Layout) void { " option, falling back to default", .{}),
switch (event) {
.namespace_in_use => {
std.debug.warn("{}: Namespace already in use.\n", .{self.getNamespace()});
self.deinit();
}, },
f64 => switch (event) {
.fixed_value => |ev| option.value = ev.value.toDouble(),
else => std.log.err("expected value of fixed type for " ++ key ++
" option, falling back to default", .{}),
},
Location => switch (event) {
.string_value => |ev| if (ev.value) |value| {
if (std.meta.stringToEnum(Location, mem.span(value))) |location| {
option.value = location;
} else {
std.log.err(
\\invalid main_location "{s}", must be "top", "bottom", "left", or "right"
, .{value});
}
},
else => std.log.err("expected value of string type for " ++ key ++
" option, falling back to default", .{}),
},
else => unreachable,
}
if (option.value != prev_value) option.output.layout.parametersChanged();
}
};
}
.layout_demand => |data| { const Output = struct {
const main_amount = self.output.main_amount.getValueOrElse(u32, 1); wl_output: *wl.Output,
const main_factor = std.math.clamp(self.output.main_factor.getValueOrElse(f64, 0.6), 0.1, 0.9); name: u32,
const view_padding = self.output.view_padding.getValueOrElse(u32, 0);
const outer_padding = self.output.outer_padding.getValueOrElse(u32, 0);
const secondary_count = if (data.view_count > main_amount) main_location: Option("main_location", Location, default_main_location) = undefined,
data.view_count - main_amount main_count: Option("main_count", u32, default_main_count) = undefined,
main_factor: Option("main_factor", f64, default_main_factor) = undefined,
view_padding: Option("view_padding", u32, default_view_padding) = undefined,
outer_padding: Option("outer_padding", u32, default_outer_padding) = undefined,
layout: *river.LayoutV1 = undefined,
fn init(output: *Output, context: *Context, wl_output: *wl.Output, name: u32) !void {
output.* = .{ .wl_output = wl_output, .name = name };
if (context.initialized) try output.initOptionsAndLayout(context);
}
fn initOptionsAndLayout(output: *Output, context: *Context) !void {
assert(context.initialized);
try output.main_location.init(context, output);
errdefer output.main_location.deinit();
try output.main_count.init(context, output);
errdefer output.main_count.deinit();
try output.main_factor.init(context, output);
errdefer output.main_factor.deinit();
try output.view_padding.init(context, output);
errdefer output.view_padding.deinit();
try output.outer_padding.init(context, output);
errdefer output.outer_padding.deinit();
output.layout = try context.layout_manager.?.getLayout(output.wl_output, "rivertile");
output.layout.setListener(*Output, layoutListener, output) catch unreachable;
}
fn deinit(output: *Output) void {
output.wl_output.release();
output.main_count.deinit();
output.main_factor.deinit();
output.view_padding.deinit();
output.outer_padding.deinit();
output.layout.destroy();
}
fn layoutListener(layout: *river.LayoutV1, event: river.LayoutV1.Event, output: *Output) void {
switch (event) {
.namespace_in_use => fatal("namespace 'rivertile' already in use.", .{}),
.layout_demand => |ev| {
const secondary_count = if (ev.view_count > output.main_count.value)
ev.view_count - output.main_count.value
else else
0; 0;
const usable_width = if (self.orientation == .left or self.orientation == .right) const usable_width = switch (output.main_location.value) {
data.usable_width - (2 * outer_padding) .left, .right => ev.usable_width - (2 * output.outer_padding.value),
else .top, .bottom => ev.usable_height - (2 * output.outer_padding.value),
data.usable_height - (2 * outer_padding); };
const usable_height = if (self.orientation == .left or self.orientation == .right) const usable_height = switch (output.main_location.value) {
data.usable_height - (2 * outer_padding) .left, .right => ev.usable_height - (2 * output.outer_padding.value),
else .top, .bottom => ev.usable_width - (2 * output.outer_padding.value),
data.usable_width - (2 * outer_padding); };
// to make things pixel-perfect, we make the first main and first secondary // to make things pixel-perfect, we make the first main and first secondary
// view slightly larger if the height is not evenly divisible // view slightly larger if the height is not evenly divisible
@ -271,18 +204,18 @@ const Layout = struct {
var secondary_height: u32 = undefined; var secondary_height: u32 = undefined;
var secondary_height_rem: u32 = undefined; var secondary_height_rem: u32 = undefined;
if (main_amount > 0 and secondary_count > 0) { if (output.main_count.value > 0 and secondary_count > 0) {
main_width = @floatToInt(u32, main_factor * @intToFloat(f64, usable_width)); main_width = @floatToInt(u32, output.main_factor.value * @intToFloat(f64, usable_width));
main_height = usable_height / main_amount; main_height = usable_height / output.main_count.value;
main_height_rem = usable_height % main_amount; main_height_rem = usable_height % output.main_count.value;
secondary_width = usable_width - main_width; secondary_width = usable_width - main_width;
secondary_height = usable_height / secondary_count; secondary_height = usable_height / secondary_count;
secondary_height_rem = usable_height % secondary_count; secondary_height_rem = usable_height % secondary_count;
} else if (main_amount > 0) { } else if (output.main_count.value > 0) {
main_width = usable_width; main_width = usable_width;
main_height = usable_height / main_amount; main_height = usable_height / output.main_count.value;
main_height_rem = usable_height % main_amount; main_height_rem = usable_height % output.main_count.value;
} else if (secondary_width > 0) { } else if (secondary_width > 0) {
main_width = 0; main_width = 0;
secondary_width = usable_width; secondary_width = usable_width;
@ -291,63 +224,63 @@ const Layout = struct {
} }
var i: u32 = 0; var i: u32 = 0;
while (i < data.view_count) : (i += 1) { while (i < ev.view_count) : (i += 1) {
var x: i32 = undefined; var x: i32 = undefined;
var y: i32 = undefined; var y: i32 = undefined;
var width: u32 = undefined; var width: u32 = undefined;
var height: u32 = undefined; var height: u32 = undefined;
if (i < main_amount) { if (i < output.main_count.value) {
x = 0; x = 0;
y = @intCast(i32, (i * main_height) + if (i > 0) main_height_rem else 0); y = @intCast(i32, (i * main_height) + if (i > 0) main_height_rem else 0);
width = main_width; width = main_width;
height = main_height + if (i == 0) main_height_rem else 0; height = main_height + if (i == 0) main_height_rem else 0;
} else { } else {
x = @intCast(i32, main_width); x = @intCast(i32, main_width);
y = @intCast(i32, (i - main_amount) * secondary_height + y = @intCast(i32, (i - output.main_count.value) * secondary_height +
if (i > main_amount) secondary_height_rem else 0); if (i > output.main_count.value) secondary_height_rem else 0);
width = secondary_width; width = secondary_width;
height = secondary_height + if (i == main_amount) secondary_height_rem else 0; height = secondary_height + if (i == output.main_count.value) secondary_height_rem else 0;
} }
x += @intCast(i32, view_padding); x += @intCast(i32, output.view_padding.value);
y += @intCast(i32, view_padding); y += @intCast(i32, output.view_padding.value);
width -= 2 * view_padding; width -= 2 * output.view_padding.value;
height -= 2 * view_padding; height -= 2 * output.view_padding.value;
switch (self.orientation) { switch (output.main_location.value) {
.left => layout.pushViewDimensions( .left => layout.pushViewDimensions(
data.serial, ev.serial,
x + @intCast(i32, outer_padding), x + @intCast(i32, output.outer_padding.value),
y + @intCast(i32, outer_padding), y + @intCast(i32, output.outer_padding.value),
width, width,
height, height,
), ),
.right => layout.pushViewDimensions( .right => layout.pushViewDimensions(
data.serial, ev.serial,
@intCast(i32, usable_width - width) - x + @intCast(i32, outer_padding), @intCast(i32, usable_width - width) - x + @intCast(i32, output.outer_padding.value),
y + @intCast(i32, outer_padding), y + @intCast(i32, output.outer_padding.value),
width, width,
height, height,
), ),
.top => layout.pushViewDimensions( .top => layout.pushViewDimensions(
data.serial, ev.serial,
y + @intCast(i32, outer_padding), y + @intCast(i32, output.outer_padding.value),
x + @intCast(i32, outer_padding), x + @intCast(i32, output.outer_padding.value),
height, height,
width, width,
), ),
.bottom => layout.pushViewDimensions( .bottom => layout.pushViewDimensions(
data.serial, ev.serial,
y + @intCast(i32, outer_padding), y + @intCast(i32, output.outer_padding.value),
@intCast(i32, usable_width - width) - x + @intCast(i32, outer_padding), @intCast(i32, usable_width - width) - x + @intCast(i32, output.outer_padding.value),
height, height,
width, width,
), ),
} }
} }
layout.commit(data.serial); layout.commit(ev.serial);
}, },
.advertise_view => {}, .advertise_view => {},
@ -366,25 +299,32 @@ pub fn main() !void {
var context: Context = .{}; var context: Context = .{};
const registry = try display.getRegistry(); const registry = try display.getRegistry();
try registry.setListener(*Context, registryListener, &context); registry.setListener(*Context, registryListener, &context) catch unreachable;
_ = try display.roundtrip(); _ = try display.roundtrip();
if (context.layout_manager == null) { if (context.layout_manager == null) {
std.debug.warn("Wayland server does not support river_layout_unstable_v1.\n", .{}); fatal("wayland compositor does not support river_layout_v1.\n", .{});
std.os.exit(1);
} }
if (context.options_manager == null) { if (context.options_manager == null) {
std.debug.warn("Wayland server does not support river_options_unstable_v1.\n", .{}); fatal("wayland compositor does not support river_options_v2.\n", .{});
std.os.exit(1);
} }
context.configureAllOutputs(); // TODO: should be @tagName(default_main_location), https://github.com/ziglang/zig/issues/3779
defer context.destroyAllOutputs(); context.options_manager.?.declareStringOption("main_location", "left");
context.options_manager.?.declareUintOption("main_count", default_main_count);
context.options_manager.?.declareFixedOption("main_factor", wl.Fixed.fromDouble(default_main_factor));
context.options_manager.?.declareUintOption("view_padding", default_view_padding);
context.options_manager.?.declareUintOption("outer_padding", default_outer_padding);
while (context.running) { context.initialized = true;
_ = try display.dispatch();
var it = context.outputs.first;
while (it) |node| : (it = node.next) {
const output = &node.data;
try output.initOptionsAndLayout(&context);
} }
while (true) _ = try display.dispatch();
} }
fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, context: *Context) void { fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, context: *Context) void {
@ -392,15 +332,28 @@ fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, context: *
.global => |global| { .global => |global| {
if (std.cstr.cmp(global.interface, river.LayoutManagerV1.getInterface().name) == 0) { if (std.cstr.cmp(global.interface, river.LayoutManagerV1.getInterface().name) == 0) {
context.layout_manager = registry.bind(global.name, river.LayoutManagerV1, 1) catch return; context.layout_manager = registry.bind(global.name, river.LayoutManagerV1, 1) catch return;
} else if (std.cstr.cmp(global.interface, zriver.OptionsManagerV1.getInterface().name) == 0) { } else if (std.cstr.cmp(global.interface, river.OptionsManagerV2.getInterface().name) == 0) {
context.options_manager = registry.bind(global.name, zriver.OptionsManagerV1, 1) catch return; context.options_manager = registry.bind(global.name, river.OptionsManagerV2, 1) catch return;
} else if (std.cstr.cmp(global.interface, wl.Output.getInterface().name) == 0) { } else if (std.cstr.cmp(global.interface, wl.Output.getInterface().name) == 0) {
context.addOutput(registry, global.name) catch { context.addOutput(registry, global.name) catch |err| fatal("failed to bind output: {}", .{err});
std.debug.warn("Failed to bind output.\n", .{}); }
context.running = false; },
}; .global_remove => |ev| {
var it = context.outputs.first;
while (it) |node| : (it = node.next) {
const output = &node.data;
if (output.name == ev.name) {
context.outputs.remove(node);
output.deinit();
gpa.destroy(node);
break;
}
} }
}, },
.global_remove => |global| {},
} }
} }
fn fatal(comptime format: []const u8, args: anytype) noreturn {
std.log.err(format, args);
std.os.exit(1);
}