git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commitdbbc744096273705158e389f51a905d357af9c19
parent0acedfeac3
authorIsaac Freund <[email protected]>
date2024-06-28 10:18
river: remove river-control-unstable-v1

 river/Control.zig                      | 143 -----------
 river/Cursor.zig                       |   3 +-
 river/Seat.zig                         |  26 +-
 river/Server.zig                       |   5 -
 river/command.zig                      | 170 -------------
 river/command/attach_mode.zig          |  61 -----
 river/command/close.zig                |  31 ---
 river/command/config.zig               | 119 ---------
 river/command/cursor.zig               |  51 ----
 river/command/declare_mode.zig         |  49 ----
 river/command/enter_mode.zig           |  63 -----
 river/command/exit.zig                 |  32 ---
 river/command/focus_follows_cursor.zig |  35 ---
 river/command/input.zig                | 121 ---------
 river/command/keyboard.zig             | 108 --------
 river/command/keyboard_group.zig       | 100 --------
 river/command/layout.zig               |  85 -------
 river/command/map.zig                  | 437 ---------------------------------
 river/command/move.zig                 | 150 -----------
 river/command/output.zig               | 135 ----------
 river/command/rule.zig                 | 265 --------------------
 river/command/set_repeat.zig           |  45 ----
 river/command/spawn.zig                |  62 -----
 river/command/tags.zig                 | 144 -----------
 river/command/toggle_float.zig         |  47 ----
 river/command/toggle_fullscreen.zig    |  38 ---
 river/command/view_operations.zig      | 145 -----------
 river/command/xcursor_theme.zig        |  34 ---
 river/command/zoom.zig                 |  85 -------
 29 files changed, 3 insertions(+), 2786 deletions(-)

diff --git a/river/Control.zig b/river/Control.zig
deleted file mode 100644
index 2b0d21f..0000000
--- a/river/Control.zig
+++ /dev/null
@@ -1,143 +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, version 3.
-//
-// 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 Control = @This();
-
-const std = @import("std");
-const mem = std.mem;
-const wlr = @import("wlroots");
-const wayland = @import("wayland");
-const wl = wayland.server.wl;
-const zriver = wayland.server.zriver;
-
-const command = @import("command.zig");
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Seat = @import("Seat.zig");
-const Server = @import("Server.zig");
-
-const ArgMap = std.AutoHashMap(struct { client: *wl.Client, id: u32 }, std.ArrayListUnmanaged([:0]const u8));
-
-global: *wl.Global,
-
-args_map: ArgMap,
-
-server_destroy: wl.Listener(*wl.Server) = wl.Listener(*wl.Server).init(handleServerDestroy),
-
-pub fn init(control: *Control) !void {
-    control.* = .{
-        .global = try wl.Global.create(server.wl_server, zriver.ControlV1, 1, *Control, control, bind),
-        .args_map = ArgMap.init(util.gpa),
-    };
-
-    server.wl_server.addDestroyListener(&control.server_destroy);
-}
-
-fn handleServerDestroy(listener: *wl.Listener(*wl.Server), _: *wl.Server) void {
-    const control: *Control = @fieldParentPtr("server_destroy", listener);
-    control.global.destroy();
-    control.args_map.deinit();
-}
-
-/// Called when a client binds our global
-fn bind(client: *wl.Client, control: *Control, version: u32, id: u32) void {
-    const control_v1 = zriver.ControlV1.create(client, version, id) catch {
-        client.postNoMemory();
-        return;
-    };
-    control.args_map.putNoClobber(.{ .client = client, .id = id }, .{}) catch {
-        control_v1.destroy();
-        client.postNoMemory();
-        return;
-    };
-    control_v1.setHandler(*Control, handleRequest, handleDestroy, control);
-}
-
-fn handleRequest(control_v1: *zriver.ControlV1, request: zriver.ControlV1.Request, control: *Control) void {
-    switch (request) {
-        .destroy => control_v1.destroy(),
-        .add_argument => |add_argument| {
-            const owned_slice = util.gpa.dupeZ(u8, mem.sliceTo(add_argument.argument, 0)) catch {
-                control_v1.getClient().postNoMemory();
-                return;
-            };
-
-            const args = control.args_map.getPtr(.{ .client = control_v1.getClient(), .id = control_v1.getId() }).?;
-            args.append(util.gpa, owned_slice) catch {
-                control_v1.getClient().postNoMemory();
-                util.gpa.free(owned_slice);
-                return;
-            };
-        },
-        .run_command => |run_command| {
-            const seat: *Seat = @ptrFromInt(wlr.Seat.Client.fromWlSeat(run_command.seat).?.seat.data);
-
-            const callback = zriver.CommandCallbackV1.create(
-                control_v1.getClient(),
-                control_v1.getVersion(),
-                run_command.callback,
-            ) catch {
-                control_v1.getClient().postNoMemory();
-                return;
-            };
-
-            const args = control.args_map.getPtr(.{ .client = control_v1.getClient(), .id = control_v1.getId() }).?;
-            defer {
-                for (args.items) |arg| util.gpa.free(arg);
-                args.items.len = 0;
-            }
-
-            var out: ?[]const u8 = null;
-            defer if (out) |s| util.gpa.free(s);
-            command.run(seat, args.items, &out) catch |err| {
-                const failure_message = switch (err) {
-                    command.Error.OutOfMemory => {
-                        callback.getClient().postNoMemory();
-                        return;
-                    },
-                    command.Error.Other => util.gpa.dupeZ(u8, out.?) catch {
-                        callback.getClient().postNoMemory();
-                        return;
-                    },
-                    else => command.errToMsg(err),
-                };
-                defer if (err == command.Error.Other) util.gpa.free(failure_message);
-                callback.destroySendFailure(failure_message);
-                return;
-            };
-
-            const success_message = if (out) |s|
-                util.gpa.dupeZ(u8, s) catch {
-                    callback.getClient().postNoMemory();
-                    return;
-                }
-            else
-                "";
-            defer if (out != null) util.gpa.free(success_message);
-            callback.destroySendSuccess(success_message);
-        },
-    }
-}
-
-/// Remove the resource from the hash map and free all stored args
-fn handleDestroy(control_v1: *zriver.ControlV1, control: *Control) void {
-    var args = control.args_map.fetchRemove(
-        .{ .client = control_v1.getClient(), .id = control_v1.getId() },
-    ).?.value;
-    for (args.items) |arg| util.gpa.free(arg);
-    args.deinit(util.gpa);
-}
diff --git a/river/Cursor.zig b/river/Cursor.zig
index 235f29c..d48da99 100644
--- a/river/Cursor.zig
+++ b/river/Cursor.zig
@@ -666,9 +666,8 @@ fn handlePointerMapping(cursor: *Cursor, event: *wlr.Pointer.event.Button, view:
             switch (mapping.action) {
                 .move => if (!fullscreen) cursor.startMove(view),
                 .resize => if (!fullscreen) cursor.startResize(view, null),
-                .command => |args| {
+                .command => |_| {
                     cursor.seat.focus(view);
-                    cursor.seat.runCommand(args);
                     // This is mildly inefficient as running the command may have already
                     // started a transaction. However we need to start one after the Seat.focus()
                     // call in the case where it didn't.
diff --git a/river/Seat.zig b/river/Seat.zig
index 257c72d..0e5638e 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -23,7 +23,6 @@ const wlr = @import("wlroots");
 const wl = @import("wayland").server.wl;
 const xkb = @import("xkbcommon");
 
-const command = @import("command.zig");
 const server = &@import("main.zig").server;
 const util = @import("util.zig");
 
@@ -397,7 +396,6 @@ pub fn handleMapping(
                 log.err("failed to update mapping repeat timer", .{});
             };
         }
-        seat.runCommand(mapping.command_args);
         return true;
     }
 
@@ -413,30 +411,11 @@ pub fn handleSwitchMapping(
     const modes = &server.config.modes;
     for (modes.items[seat.mode_id].switch_mappings.items) |mapping| {
         if (std.meta.eql(mapping.switch_type, switch_type) and std.meta.eql(mapping.switch_state, switch_state)) {
-            seat.runCommand(mapping.command_args);
+            // send trigger
         }
     }
 }
 
-pub fn runCommand(seat: *Seat, args: []const [:0]const u8) void {
-    var out: ?[]const u8 = null;
-    defer if (out) |s| util.gpa.free(s);
-    command.run(seat, args, &out) catch |err| {
-        const failure_message = switch (err) {
-            command.Error.Other => out.?,
-            else => command.errToMsg(err),
-        };
-        std.log.scoped(.command).err("{s}: {s}", .{ args[0], failure_message });
-        return;
-    };
-    if (out) |s| {
-        const stdout = std.io.getStdOut().writer();
-        stdout.print("{s}", .{s}) catch |err| {
-            std.log.scoped(.command).err("{s}: write to stdout failed {}", .{ args[0], err });
-        };
-    }
-}
-
 pub fn clearRepeatingMapping(seat: *Seat) void {
     seat.mapping_repeat_timer.timerUpdate(0) catch {
         log.err("failed to clear mapping repeat timer", .{});
@@ -446,13 +425,12 @@ pub fn clearRepeatingMapping(seat: *Seat) void {
 
 /// Repeat key mapping
 fn handleMappingRepeatTimeout(seat: *Seat) c_int {
-    if (seat.repeating_mapping) |mapping| {
+    if (seat.repeating_mapping) |_| {
         const rate = server.config.repeat_rate;
         const ms_delay = if (rate > 0) 1000 / rate else 0;
         seat.mapping_repeat_timer.timerUpdate(ms_delay) catch {
             log.err("failed to update mapping repeat timer", .{});
         };
-        seat.runCommand(mapping.command_args);
     }
     return 0;
 }
diff --git a/river/Server.zig b/river/Server.zig
index 6a25d34..f0d626e 100644
--- a/river/Server.zig
+++ b/river/Server.zig
@@ -27,7 +27,6 @@ const c = @import("c.zig");
 const util = @import("util.zig");
 
 const Config = @import("Config.zig");
-const Control = @import("Control.zig");
 const IdleInhibitManager = @import("IdleInhibitManager.zig");
 const InputManager = @import("InputManager.zig");
 const LayerSurface = @import("LayerSurface.zig");
@@ -86,7 +85,6 @@ foreign_toplevel_manager: *wlr.ForeignToplevelManagerV1,
 input_manager: InputManager,
 root: Root,
 config: Config,
-control: Control,
 layout_manager: LayoutManager,
 idle_inhibit_manager: IdleInhibitManager,
 lock_manager: LockManager,
@@ -159,7 +157,6 @@ pub fn init(server: *Server, runtime_xwayland: bool) !void {
 
         .root = undefined,
         .input_manager = undefined,
-        .control = undefined,
         .layout_manager = undefined,
         .idle_inhibit_manager = undefined,
         .lock_manager = undefined,
@@ -182,7 +179,6 @@ pub fn init(server: *Server, runtime_xwayland: bool) !void {
 
     try server.root.init();
     try server.input_manager.init();
-    try server.control.init();
     try server.layout_manager.init();
     try server.idle_inhibit_manager.init();
     try server.lock_manager.init();
@@ -320,7 +316,6 @@ fn blocklist(server: *Server, global: *const wl.Global) bool {
         global == server.export_dmabuf_manager.global or
         global == server.data_control_manager.global or
         global == server.layout_manager.global or
-        global == server.control.global or
         global == server.root.output_manager.global or
         global == server.root.power_manager.global or
         global == server.root.gamma_control_manager.global or
diff --git a/river/command.zig b/river/command.zig
deleted file mode 100644
index 6ea5b96..0000000
--- a/river/command.zig
+++ /dev/null
@@ -1,170 +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, version 3.
-//
-// 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 assert = std.debug.assert;
-
-const Seat = @import("Seat.zig");
-
-pub const Direction = enum {
-    next,
-    previous,
-};
-
-pub const PhysicalDirection = enum {
-    up,
-    down,
-    left,
-    right,
-};
-
-pub const Orientation = enum {
-    horizontal,
-    vertical,
-};
-
-const command_impls = std.StaticStringMap(
-    *const fn (*Seat, []const [:0]const u8, *?[]const u8) Error!void,
-).initComptime(
-    .{
-        // zig fmt: off
-        .{ "attach-mode",               @import("command/attach_mode.zig").defaultAttachMode },
-        .{ "background-color",          @import("command/config.zig").backgroundColor },
-        .{ "border-color-focused",      @import("command/config.zig").borderColorFocused },
-        .{ "border-color-unfocused",    @import("command/config.zig").borderColorUnfocused },
-        .{ "border-color-urgent",       @import("command/config.zig").borderColorUrgent },
-        .{ "border-width",              @import("command/config.zig").borderWidth },
-        .{ "close",                     @import("command/close.zig").close },
-        .{ "declare-mode",              @import("command/declare_mode.zig").declareMode },
-        .{ "default-attach-mode",       @import("command/attach_mode.zig").defaultAttachMode },
-        .{ "default-layout",            @import("command/layout.zig").defaultLayout },
-        .{ "enter-mode",                @import("command/enter_mode.zig").enterMode },
-        .{ "exit",                      @import("command/exit.zig").exit },
-        .{ "focus-follows-cursor",      @import("command/focus_follows_cursor.zig").focusFollowsCursor },
-        .{ "focus-output",              @import("command/output.zig").focusOutput },
-        .{ "focus-previous-tags",       @import("command/tags.zig").focusPreviousTags },
-        .{ "focus-view",                @import("command/view_operations.zig").focusView },
-        .{ "hide-cursor",               @import("command/cursor.zig").cursor },
-        .{ "input",                     @import("command/input.zig").input },
-        .{ "keyboard-group-add",        @import("command/keyboard_group.zig").keyboardGroupAdd },
-        .{ "keyboard-group-create",     @import("command/keyboard_group.zig").keyboardGroupCreate },
-        .{ "keyboard-group-destroy",    @import("command/keyboard_group.zig").keyboardGroupDestroy },
-        .{ "keyboard-group-remove",     @import("command/keyboard_group.zig").keyboardGroupRemove },
-        .{ "keyboard-layout",           @import("command/keyboard.zig").keyboardLayout },
-        .{ "keyboard-layout-file",      @import("command/keyboard.zig").keyboardLayoutFile },
-        .{ "list-input-configs",        @import("command/input.zig").listInputConfigs},
-        .{ "list-inputs",               @import("command/input.zig").listInputs },
-        .{ "list-rules",                @import("command/rule.zig").listRules},
-        .{ "map",                       @import("command/map.zig").map },
-        .{ "map-pointer",               @import("command/map.zig").mapPointer },
-        .{ "map-switch",                @import("command/map.zig").mapSwitch },
-        .{ "move",                      @import("command/move.zig").move },
-        .{ "output-attach-mode",        @import("command/attach_mode.zig").outputAttachMode },
-        .{ "output-layout",             @import("command/layout.zig").outputLayout },
-        .{ "resize",                    @import("command/move.zig").resize },
-        .{ "rule-add",                  @import("command/rule.zig").ruleAdd },
-        .{ "rule-del",                  @import("command/rule.zig").ruleDel },
-        .{ "send-layout-cmd",           @import("command/layout.zig").sendLayoutCmd },
-        .{ "send-to-output",            @import("command/output.zig").sendToOutput },
-        .{ "send-to-previous-tags",     @import("command/tags.zig").sendToPreviousTags },
-        .{ "set-cursor-warp",           @import("command/config.zig").setCursorWarp },
-        .{ "set-focused-tags",          @import("command/tags.zig").setFocusedTags },
-        .{ "set-repeat",                @import("command/set_repeat.zig").setRepeat },
-        .{ "set-view-tags",             @import("command/tags.zig").setViewTags },
-        .{ "snap",                      @import("command/move.zig").snap },
-        .{ "spawn",                     @import("command/spawn.zig").spawn },
-        .{ "spawn-tagmask",             @import("command/tags.zig").spawnTagmask },
-        .{ "swap",                      @import("command/view_operations.zig").swap},
-        .{ "toggle-float",              @import("command/toggle_float.zig").toggleFloat },
-        .{ "toggle-focused-tags",       @import("command/tags.zig").toggleFocusedTags },
-        .{ "toggle-fullscreen",         @import("command/toggle_fullscreen.zig").toggleFullscreen },
-        .{ "toggle-view-tags",          @import("command/tags.zig").toggleViewTags },
-        .{ "unmap",                     @import("command/map.zig").unmap },
-        .{ "unmap-pointer",             @import("command/map.zig").unmapPointer },
-        .{ "unmap-switch",              @import("command/map.zig").unmapSwitch },
-        .{ "xcursor-theme",             @import("command/xcursor_theme.zig").xcursorTheme },
-        .{ "zoom",                      @import("command/zoom.zig").zoom },
-        // zig fmt: on
-    },
-);
-
-pub const Error = error{
-    NoCommand,
-    UnknownCommand,
-    NotEnoughArguments,
-    TooManyArguments,
-    OutOfBounds,
-    Overflow,
-    InvalidButton,
-    InvalidCharacter,
-    InvalidDirection,
-    InvalidGlob,
-    InvalidPhysicalDirection,
-    InvalidOutputIndicator,
-    InvalidOrientation,
-    InvalidRgba,
-    InvalidValue,
-    CannotReadFile,
-    CannotParseFile,
-    UnknownOption,
-    ConflictingOptions,
-    OutOfMemory,
-    Other,
-};
-
-/// Run a command for the given Seat. The `args` parameter is similar to the
-/// classic argv in that the command to be run is passed as the first argument.
-/// The optional slice passed as the out parameter must initially be set to
-/// null. If the command produces output or Error.Other is returned, the slice
-/// will be set to the output of the command or a failure message, respectively.
-/// The caller is then responsible for freeing that slice, which will be
-/// allocated using the provided allocator.
-pub fn run(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    assert(out.* == null);
-    if (args.len == 0) return Error.NoCommand;
-    const impl_fn = command_impls.get(args[0]) orelse return Error.UnknownCommand;
-    try impl_fn(seat, args, out);
-}
-
-/// Return a short error message for the given error. Passing Error.Other is invalid.
-pub fn errToMsg(err: Error) [:0]const u8 {
-    return switch (err) {
-        Error.NoCommand => "no command given",
-        Error.UnknownCommand => "unknown command",
-        Error.UnknownOption => "unknown option",
-        Error.ConflictingOptions => "options conflict",
-        Error.NotEnoughArguments => "not enough arguments",
-        Error.TooManyArguments => "too many arguments",
-        Error.OutOfBounds, Error.Overflow => "value out of bounds",
-        Error.InvalidButton => "invalid button",
-        Error.InvalidCharacter => "invalid character in argument",
-        Error.InvalidDirection => "invalid direction. Must be 'next' or 'previous'",
-        Error.InvalidGlob => "invalid glob. '*' is only allowed as the first and/or last character",
-        Error.InvalidPhysicalDirection => "invalid direction. Must be 'up', 'down', 'left' or 'right'",
-        Error.InvalidOutputIndicator => "invalid indicator for an output. Must be 'next', 'previous', 'up', 'down', 'left', 'right' or a valid output name",
-        Error.InvalidOrientation => "invalid orientation. Must be 'horizontal', or 'vertical'",
-        Error.InvalidRgba => "invalid color format, must be hexadecimal 0xRRGGBB or 0xRRGGBBAA",
-        Error.InvalidValue => "invalid value",
-        Error.CannotReadFile => "cannot read file",
-        Error.CannotParseFile => "cannot parse file",
-        Error.OutOfMemory => "out of memory",
-        Error.Other => unreachable,
-    };
-}
diff --git a/river/command/attach_mode.zig b/river/command/attach_mode.zig
deleted file mode 100644
index 6ffc615..0000000
--- a/river/command/attach_mode.zig
+++ /dev/null
@@ -1,61 +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, version 3.
-//
-// 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 mem = std.mem;
-const meta = std.meta;
-
-const server = &@import("../main.zig").server;
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-const Config = @import("../Config.zig");
-
-fn parseAttachMode(args: []const [:0]const u8) Error!Config.AttachMode {
-    if (args.len < 2) return Error.NotEnoughArguments;
-
-    const tag = meta.stringToEnum(meta.Tag(Config.AttachMode), args[1]) orelse return Error.UnknownOption;
-    switch (tag) {
-        inline .top, .bottom, .above, .below => |mode| {
-            if (args.len > 2) return Error.TooManyArguments;
-
-            return mode;
-        },
-        .after => {
-            if (args.len < 3) return Error.NotEnoughArguments;
-            if (args.len > 3) return Error.TooManyArguments;
-
-            return .{ .after = try std.fmt.parseInt(u32, args[2], 10) };
-        },
-    }
-}
-
-pub fn outputAttachMode(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    const output = seat.focused_output orelse return;
-    output.attach_mode = try parseAttachMode(args);
-}
-
-pub fn defaultAttachMode(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    server.config.default_attach_mode = try parseAttachMode(args);
-}
diff --git a/river/command/close.zig b/river/command/close.zig
deleted file mode 100644
index 6deaad5..0000000
--- a/river/command/close.zig
+++ /dev/null
@@ -1,31 +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, version 3.
-//
-// 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 Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-/// Close the focused view, if any.
-pub fn close(
-    seat: *Seat,
-    _: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    // Note: we don't call arrange() here as it will be called
-    // automatically when the view is unmapped.
-    if (seat.focused == .view) seat.focused.view.close();
-}
diff --git a/river/command/config.zig b/river/command/config.zig
deleted file mode 100644
index a97e233..0000000
--- a/river/command/config.zig
+++ /dev/null
@@ -1,119 +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, version 3.
-//
-// 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 fmt = std.fmt;
-const mem = std.mem;
-
-const server = &@import("../main.zig").server;
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-const Config = @import("../Config.zig");
-
-pub fn borderWidth(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    server.config.border_width = try fmt.parseInt(u31, args[1], 10);
-    server.root.applyPending();
-}
-
-pub fn backgroundColor(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    server.config.background_color = try parseRgba(args[1]);
-    var it = server.root.all_outputs.iterator(.forward);
-    while (it.next()) |output| {
-        output.layers.background_color_rect.setColor(&server.config.background_color);
-    }
-}
-
-pub fn borderColorFocused(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    server.config.border_color_focused = try parseRgba(args[1]);
-    server.root.applyPending();
-}
-
-pub fn borderColorUnfocused(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    server.config.border_color_unfocused = try parseRgba(args[1]);
-    server.root.applyPending();
-}
-
-pub fn borderColorUrgent(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    server.config.border_color_urgent = try parseRgba(args[1]);
-    server.root.applyPending();
-}
-
-pub fn setCursorWarp(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-    server.config.warp_cursor = std.meta.stringToEnum(Config.WarpCursorMode, args[1]) orelse
-        return Error.UnknownOption;
-}
-
-/// Parse a color in the format 0xRRGGBB or 0xRRGGBBAA. Returned color has premultiplied alpha.
-fn parseRgba(string: []const u8) ![4]f32 {
-    if (string.len != 8 and string.len != 10) return error.InvalidRgba;
-    if (string[0] != '0' or string[1] != 'x') return error.InvalidRgba;
-
-    const r = try fmt.parseInt(u8, string[2..4], 16);
-    const g = try fmt.parseInt(u8, string[4..6], 16);
-    const b = try fmt.parseInt(u8, string[6..8], 16);
-    const a = if (string.len == 10) try fmt.parseInt(u8, string[8..10], 16) else 255;
-
-    const alpha = @as(f32, @floatFromInt(a)) / 255.0;
-
-    return [4]f32{
-        @as(f32, @floatFromInt(r)) / 255.0 * alpha,
-        @as(f32, @floatFromInt(g)) / 255.0 * alpha,
-        @as(f32, @floatFromInt(b)) / 255.0 * alpha,
-        alpha,
-    };
-}
diff --git a/river/command/cursor.zig b/river/command/cursor.zig
deleted file mode 100644
index 33b7b50..0000000
--- a/river/command/cursor.zig
+++ /dev/null
@@ -1,51 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2022 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 util = @import("../util.zig");
-
-const server = &@import("../main.zig").server;
-
-const Config = @import("../Config.zig");
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-pub fn cursor(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (std.mem.eql(u8, "timeout", args[1])) {
-        if (args.len < 3) return Error.NotEnoughArguments;
-        if (args.len > 3) return Error.TooManyArguments;
-        server.config.cursor_hide_timeout = try std.fmt.parseInt(u31, args[2], 10);
-        var seat_it = server.input_manager.seats.first;
-        while (seat_it) |seat_node| : (seat_it = seat_node.next) {
-            const seat = &seat_node.data;
-            seat.cursor.unhide();
-        }
-    } else if (std.mem.eql(u8, "when-typing", args[1])) {
-        if (args.len < 3) return Error.NotEnoughArguments;
-        if (args.len > 3) return Error.TooManyArguments;
-        server.config.cursor_hide_when_typing = std.meta.stringToEnum(Config.HideCursorWhenTypingMode, args[2]) orelse
-            return Error.UnknownOption;
-    } else {
-        return Error.UnknownOption;
-    }
-}
diff --git a/river/command/declare_mode.zig b/river/command/declare_mode.zig
deleted file mode 100644
index 3ebbd03..0000000
--- a/river/command/declare_mode.zig
+++ /dev/null
@@ -1,49 +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, version 3.
-//
-// 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 server = &@import("../main.zig").server;
-const util = @import("../util.zig");
-
-const Mode = @import("../Mode.zig");
-const Error = @import("../command.zig").Error;
-const Mapping = @import("../Mapping.zig");
-const Seat = @import("../Seat.zig");
-
-/// Declare a new keymap mode
-pub fn declareMode(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    const config = &server.config;
-    const new_mode_name = args[1];
-
-    if (config.mode_to_id.get(new_mode_name) != null) return;
-
-    try config.mode_to_id.ensureUnusedCapacity(1);
-    try config.modes.ensureUnusedCapacity(util.gpa, 1);
-
-    const owned_name = try util.gpa.dupeZ(u8, new_mode_name);
-
-    const id: u32 = @intCast(config.modes.items.len);
-    config.mode_to_id.putAssumeCapacityNoClobber(owned_name, id);
-    config.modes.appendAssumeCapacity(.{ .name = owned_name });
-}
diff --git a/river/command/enter_mode.zig b/river/command/enter_mode.zig
deleted file mode 100644
index c982e60..0000000
--- a/river/command/enter_mode.zig
+++ /dev/null
@@ -1,63 +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, version 3.
-//
-// 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 server = &@import("../main.zig").server;
-const util = @import("../util.zig");
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-/// Switch to the given mode
-pub fn enterMode(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    if (seat.mode_id == 1) {
-        out.* = try std.fmt.allocPrint(
-            util.gpa,
-            "manually exiting mode 'locked' is not allowed",
-            .{},
-        );
-        return Error.Other;
-    }
-
-    const target_mode = args[1];
-    const mode_id = server.config.mode_to_id.get(target_mode) orelse {
-        out.* = try std.fmt.allocPrint(
-            util.gpa,
-            "cannot enter non-existant mode '{s}'",
-            .{target_mode},
-        );
-        return Error.Other;
-    };
-
-    if (mode_id == 1) {
-        out.* = try std.fmt.allocPrint(
-            util.gpa,
-            "manually entering mode 'locked' is not allowed",
-            .{},
-        );
-        return Error.Other;
-    }
-
-    seat.enterMode(mode_id);
-}
diff --git a/river/command/exit.zig b/river/command/exit.zig
deleted file mode 100644
index 7d9ba0c..0000000
--- a/river/command/exit.zig
+++ /dev/null
@@ -1,32 +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, version 3.
-//
-// 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 server = &@import("../main.zig").server;
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-/// Exit the compositor, terminating the wayland session.
-pub fn exit(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len > 1) return Error.TooManyArguments;
-    server.wl_server.terminate();
-}
diff --git a/river/command/focus_follows_cursor.zig b/river/command/focus_follows_cursor.zig
deleted file mode 100644
index a374b8b..0000000
--- a/river/command/focus_follows_cursor.zig
+++ /dev/null
@@ -1,35 +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, version 3.
-//
-// 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 server = &@import("../main.zig").server;
-
-const Config = @import("../Config.zig");
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-pub fn focusFollowsCursor(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    server.config.focus_follows_cursor =
-        std.meta.stringToEnum(Config.FocusFollowsCursorMode, args[1]) orelse return Error.UnknownOption;
-}
diff --git a/river/command/input.zig b/river/command/input.zig
deleted file mode 100644
index acc7927..0000000
--- a/river/command/input.zig
+++ /dev/null
@@ -1,121 +0,0 @@
-// 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, version 3.
-//
-// 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 mem = std.mem;
-const sort = std.sort;
-
-const globber = @import("globber");
-
-const server = &@import("../main.zig").server;
-const util = @import("../util.zig");
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-const InputConfig = @import("../InputConfig.zig");
-
-pub fn listInputs(
-    _: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    if (args.len > 1) return error.TooManyArguments;
-
-    var input_list = std.ArrayList(u8).init(util.gpa);
-    const writer = input_list.writer();
-    var prev = false;
-
-    var it = server.input_manager.devices.iterator(.forward);
-    while (it.next()) |device| {
-        const configured = for (server.input_manager.configs.items) |*input_config| {
-            if (globber.match(device.identifier, input_config.glob)) {
-                break true;
-            }
-        } else false;
-
-        if (prev) try input_list.appendSlice("\n");
-        prev = true;
-
-        try writer.print("{s}\n\tconfigured: {}\n", .{
-            device.identifier,
-            configured,
-        });
-    }
-
-    out.* = try input_list.toOwnedSlice();
-}
-
-pub fn listInputConfigs(
-    _: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    if (args.len > 1) return error.TooManyArguments;
-
-    var input_list = std.ArrayList(u8).init(util.gpa);
-    const writer = input_list.writer();
-
-    for (server.input_manager.configs.items, 0..) |*input_config, i| {
-        if (i > 0) try writer.writeByte('\n');
-        try input_config.write(writer);
-    }
-
-    out.* = try input_list.toOwnedSlice();
-}
-
-pub fn input(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 4) return Error.NotEnoughArguments;
-    if (args.len > 4) return Error.TooManyArguments;
-
-    try globber.validate(args[1]);
-
-    // Try to find an existing InputConfig with matching glob pattern, or create
-    // a new one if none was found.
-    for (server.input_manager.configs.items) |*input_config| {
-        if (mem.eql(u8, input_config.glob, args[1])) {
-            try input_config.parse(args[2], args[3]);
-            break;
-        }
-    } else {
-        var input_config: InputConfig = .{
-            .glob = try util.gpa.dupe(u8, args[1]),
-        };
-        errdefer util.gpa.free(input_config.glob);
-
-        try server.input_manager.configs.ensureUnusedCapacity(1);
-
-        try input_config.parse(args[2], args[3]);
-
-        server.input_manager.configs.appendAssumeCapacity(input_config);
-    }
-
-    // Sort input configs from most general to least general
-    sort.insertion(InputConfig, server.input_manager.configs.items, {}, lessThan);
-
-    // We need to update all input device matching the glob. The user may
-    // add an input configuration at an arbitrary position in the generality
-    // ordered list, so the simplest way to ensure the device is configured
-    // correctly is to apply all input configurations again, in order.
-    server.input_manager.reconfigureDevices();
-}
-
-fn lessThan(_: void, a: InputConfig, b: InputConfig) bool {
-    return globber.order(a.glob, b.glob) == .gt;
-}
diff --git a/river/command/keyboard.zig b/river/command/keyboard.zig
deleted file mode 100644
index c09db1f..0000000
--- a/river/command/keyboard.zig
+++ /dev/null
@@ -1,108 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2022 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, version 3.
-//
-// 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 mem = std.mem;
-
-const xkb = @import("xkbcommon");
-const flags = @import("flags");
-
-const server = &@import("../main.zig").server;
-const util = @import("../util.zig");
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-pub fn keyboardLayout(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    const result = flags.parser([:0]const u8, &.{
-        .{ .name = "rules", .kind = .arg },
-        .{ .name = "model", .kind = .arg },
-        .{ .name = "variant", .kind = .arg },
-        .{ .name = "options", .kind = .arg },
-    }).parse(args[1..]) catch {
-        return error.InvalidValue;
-    };
-    if (result.args.len < 1) return Error.NotEnoughArguments;
-    if (result.args.len > 1) return Error.TooManyArguments;
-
-    const rule_names = xkb.RuleNames{
-        .layout = result.args[0],
-        // TODO(zig) these should eventually coerce without this hack.
-        .rules = if (result.flags.rules) |s| s else null,
-        .model = if (result.flags.model) |s| s else null,
-        .variant = if (result.flags.variant) |s| s else null,
-        .options = if (result.flags.options) |s| s else null,
-    };
-
-    const new_keymap = xkb.Keymap.newFromNames(
-        server.config.xkb_context,
-        &rule_names,
-        .no_flags,
-    ) orelse return error.InvalidValue;
-    defer new_keymap.unref();
-
-    applyLayout(new_keymap);
-}
-
-pub fn keyboardLayoutFile(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    const file = std.fs.cwd().openFile(args[1], .{}) catch return error.CannotReadFile;
-    defer file.close();
-
-    // 1 GiB is arbitrarily chosen as an exceedingly large but not infinite upper bound.
-    const file_bytes = file.readToEndAlloc(util.gpa, 1024 * 1024 * 1024) catch |err| {
-        switch (err) {
-            error.FileTooBig, error.OutOfMemory => return error.OutOfMemory,
-            else => return error.CannotReadFile,
-        }
-    };
-    defer util.gpa.free(file_bytes);
-
-    const new_keymap = xkb.Keymap.newFromBuffer(
-        server.config.xkb_context,
-        file_bytes.ptr,
-        file_bytes.len,
-        .text_v1,
-        .no_flags,
-    ) orelse return error.CannotParseFile;
-    defer new_keymap.unref();
-
-    applyLayout(new_keymap);
-}
-
-fn applyLayout(new_keymap: *xkb.Keymap) void {
-    server.config.keymap.unref();
-    server.config.keymap = new_keymap.ref();
-
-    var it = server.input_manager.devices.iterator(.forward);
-    while (it.next()) |device| {
-        if (device.wlr_device.type != .keyboard) continue;
-        const wlr_keyboard = device.wlr_device.toKeyboard();
-        // wlroots will log an error if this fails and there's unfortunately
-        // nothing we can really do in the case of failure.
-        _ = wlr_keyboard.setKeymap(new_keymap);
-    }
-}
diff --git a/river/command/keyboard_group.zig b/river/command/keyboard_group.zig
deleted file mode 100644
index 442fbdc..0000000
--- a/river/command/keyboard_group.zig
+++ /dev/null
@@ -1,100 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2022 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, version 3.
-//
-// 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 mem = std.mem;
-
-const globber = @import("globber");
-
-const server = &@import("../main.zig").server;
-const util = @import("../util.zig");
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-const KeyboardGroup = @import("../KeyboardGroup.zig");
-
-pub fn keyboardGroupCreate(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    if (keyboardGroupFromName(seat, args[1]) != null) {
-        const msg = try util.gpa.dupe(u8, "error: failed to create keybaord group: group of same name already exists\n");
-        out.* = msg;
-        return;
-    }
-
-    try KeyboardGroup.create(seat, args[1]);
-}
-
-pub fn keyboardGroupDestroy(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-    const group = keyboardGroupFromName(seat, args[1]) orelse {
-        const msg = try util.gpa.dupe(u8, "error: no keyboard group with that name exists\n");
-        out.* = msg;
-        return;
-    };
-    group.destroy();
-}
-
-pub fn keyboardGroupAdd(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    if (args.len < 3) return Error.NotEnoughArguments;
-    if (args.len > 3) return Error.TooManyArguments;
-
-    const group = keyboardGroupFromName(seat, args[1]) orelse {
-        const msg = try util.gpa.dupe(u8, "error: no keyboard group with that name exists\n");
-        out.* = msg;
-        return;
-    };
-    try globber.validate(args[2]);
-    try group.addIdentifier(args[2]);
-}
-
-pub fn keyboardGroupRemove(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    if (args.len < 3) return Error.NotEnoughArguments;
-    if (args.len > 3) return Error.TooManyArguments;
-
-    const group = keyboardGroupFromName(seat, args[1]) orelse {
-        const msg = try util.gpa.dupe(u8, "error: no keyboard group with that name exists\n");
-        out.* = msg;
-        return;
-    };
-    try group.removeIdentifier(args[2]);
-}
-
-fn keyboardGroupFromName(seat: *Seat, name: []const u8) ?*KeyboardGroup {
-    var it = seat.keyboard_groups.first;
-    while (it) |node| : (it = node.next) {
-        if (mem.eql(u8, node.data.name, name)) return &node.data;
-    }
-    return null;
-}
diff --git a/river/command/layout.zig b/river/command/layout.zig
deleted file mode 100644
index 798dff6..0000000
--- a/river/command/layout.zig
+++ /dev/null
@@ -1,85 +0,0 @@
-// 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, version 3.
-//
-// 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 mem = std.mem;
-const wl = @import("wayland").server.wl;
-const util = @import("../util.zig");
-
-const server = &@import("../main.zig").server;
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-pub fn outputLayout(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    const output = seat.focused_output orelse return;
-    const old_layout_namespace = output.layout_namespace;
-    output.layout_namespace = try util.gpa.dupe(u8, args[1]);
-    if (old_layout_namespace) |old| util.gpa.free(old);
-    output.handleLayoutNamespaceChange();
-}
-
-pub fn defaultLayout(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    const old_default_layout_namespace = server.config.default_layout_namespace;
-    server.config.default_layout_namespace = try util.gpa.dupe(u8, args[1]);
-    util.gpa.free(old_default_layout_namespace);
-
-    var it = server.root.all_outputs.iterator(.forward);
-    while (it.next()) |output| {
-        if (output.layout_namespace == null) output.handleLayoutNamespaceChange();
-    }
-}
-
-/// riverctl send-layout-cmd rivertile "mod-main-count 1"
-/// riverctl send-layout-cmd rivertile "mod-main-factor -0.1"
-/// riverctl send-layout-cmd rivertile "main-location top"
-pub fn sendLayoutCmd(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 3) return Error.NotEnoughArguments;
-    if (args.len > 3) return Error.TooManyArguments;
-
-    const output = seat.focused_output orelse return;
-    const target_namespace = args[1];
-
-    var it = output.layouts.first;
-    const layout = while (it) |node| : (it = node.next) {
-        const layout = &node.data;
-        if (mem.eql(u8, layout.namespace, target_namespace)) break layout;
-    } else return;
-
-    if (layout.layout_v3.getVersion() >= 2) {
-        layout.layout_v3.sendUserCommandTags(output.pending.tags);
-    }
-    layout.layout_v3.sendUserCommand(args[2]);
-    if (layout == output.layout) server.root.applyPending();
-}
diff --git a/river/command/map.zig b/river/command/map.zig
deleted file mode 100644
index e8ded57..0000000
--- a/river/command/map.zig
+++ /dev/null
@@ -1,437 +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, version 3.
-//
-// 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 fmt = std.fmt;
-const mem = std.mem;
-const meta = std.meta;
-const wlr = @import("wlroots");
-const xkb = @import("xkbcommon");
-const flags = @import("flags");
-
-const c = @import("../c.zig");
-const server = &@import("../main.zig").server;
-const util = @import("../util.zig");
-
-const Error = @import("../command.zig").Error;
-const Mapping = @import("../Mapping.zig");
-const PointerMapping = @import("../PointerMapping.zig");
-const SwitchMapping = @import("../SwitchMapping.zig");
-const Switch = @import("../Switch.zig");
-const Seat = @import("../Seat.zig");
-
-/// Create a new mapping for a given mode
-///
-/// Example:
-/// map normal Mod4+Shift Return spawn foot
-pub fn map(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    const result = flags.parser([:0]const u8, &.{
-        .{ .name = "release", .kind = .boolean },
-        .{ .name = "repeat", .kind = .boolean },
-        .{ .name = "layout", .kind = .arg },
-    }).parse(args[1..]) catch {
-        return error.InvalidValue;
-    };
-    if (result.args.len < 4) return Error.NotEnoughArguments;
-
-    if (result.flags.release and result.flags.repeat) return Error.ConflictingOptions;
-
-    const layout_index = blk: {
-        if (result.flags.layout) |layout_raw| {
-            break :blk try fmt.parseInt(u32, layout_raw, 10);
-        } else {
-            break :blk null;
-        }
-    };
-
-    const mode_raw = result.args[0];
-    const modifiers_raw = result.args[1];
-    const keysym_raw = result.args[2];
-    const command = result.args[3..];
-
-    const mode_id = try modeNameToId(mode_raw, out);
-    const modifiers = try parseModifiers(modifiers_raw, out);
-    const keysym = try parseKeysym(keysym_raw, out);
-
-    const mode_mappings = &server.config.modes.items[mode_id].mappings;
-
-    const new = try Mapping.init(
-        keysym,
-        modifiers,
-        command,
-        .{
-            .release = result.flags.release,
-            .repeat = result.flags.repeat,
-            .layout_index = layout_index,
-        },
-    );
-    errdefer new.deinit();
-
-    if (mappingExists(mode_mappings, modifiers, keysym, result.flags.release)) |current| {
-        mode_mappings.items[current].deinit();
-        mode_mappings.items[current] = new;
-        // Warn user if they overwrote an existing keybinding using riverctl.
-        const opts = if (result.flags.release) "-release " else "";
-        out.* = try fmt.allocPrint(
-            util.gpa,
-            "overwrote an existing keybinding: {s} {s}{s} {s}",
-            .{ mode_raw, opts, modifiers_raw, keysym_raw },
-        );
-    } else {
-        // Repeating mappings borrow the Mapping directly. To prevent a
-        // possible crash if the Mapping ArrayList is reallocated, stop any
-        // currently repeating mappings.
-        seat.clearRepeatingMapping();
-        try mode_mappings.append(util.gpa, new);
-    }
-}
-
-/// Create a new switch mapping for a given mode
-///
-/// Example:
-/// map-switch normal lid close spawn "wlr-randr --output eDP-1 --off"
-pub fn mapSwitch(
-    _: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    if (args.len < 5) return Error.NotEnoughArguments;
-
-    const mode_id = try modeNameToId(args[1], out);
-    const switch_type = try parseSwitchType(args[2], out);
-    const switch_state = try parseSwitchState(switch_type, args[3], out);
-
-    const new = try SwitchMapping.init(switch_type, switch_state, args[4..]);
-    errdefer new.deinit();
-
-    const mode_mappings = &server.config.modes.items[mode_id].switch_mappings;
-
-    if (switchMappingExists(mode_mappings, switch_type, switch_state)) |current| {
-        mode_mappings.items[current].deinit();
-        mode_mappings.items[current] = new;
-        // Warn user if they overwrote an existing keybinding using riverctl.
-        out.* = try std.fmt.allocPrint(
-            util.gpa,
-            "overwrote an existing keybinding: map-switch {s} {s} {s}",
-            .{ args[1], args[2], args[3] },
-        );
-    } else {
-        try mode_mappings.append(util.gpa, new);
-    }
-}
-
-/// Create a new pointer mapping for a given mode
-///
-/// Example:
-/// map-pointer normal Mod4 BTN_LEFT move-view
-pub fn mapPointer(
-    _: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    if (args.len < 5) return Error.NotEnoughArguments;
-
-    const mode_id = try modeNameToId(args[1], out);
-    const modifiers = try parseModifiers(args[2], out);
-    const event_code = try parseEventCode(args[3], out);
-
-    const action: meta.Tag(PointerMapping.Action) = blk: {
-        if (mem.eql(u8, args[4], "move-view")) {
-            break :blk .move;
-        } else if (mem.eql(u8, args[4], "resize-view")) {
-            break :blk .resize;
-        } else {
-            break :blk .command;
-        }
-    };
-
-    if (action != .command and args.len > 5) return Error.TooManyArguments;
-
-    var new = try PointerMapping.init(
-        event_code,
-        modifiers,
-        action,
-        args[4..],
-    );
-    errdefer new.deinit();
-
-    const mode_pointer_mappings = &server.config.modes.items[mode_id].pointer_mappings;
-    if (pointerMappingExists(mode_pointer_mappings, modifiers, event_code)) |current| {
-        mode_pointer_mappings.items[current].deinit();
-        mode_pointer_mappings.items[current] = new;
-    } else {
-        try mode_pointer_mappings.append(util.gpa, new);
-    }
-}
-
-fn modeNameToId(mode_name: []const u8, out: *?[]const u8) !usize {
-    const config = &server.config;
-    return config.mode_to_id.get(mode_name) orelse {
-        out.* = try fmt.allocPrint(
-            util.gpa,
-            "cannot add/remove mapping to/from non-existant mode '{s}'",
-            .{mode_name},
-        );
-        return Error.Other;
-    };
-}
-
-/// Returns the index of the Mapping with matching modifiers, keysym and release, if any.
-fn mappingExists(
-    mappings: *std.ArrayListUnmanaged(Mapping),
-    modifiers: wlr.Keyboard.ModifierMask,
-    keysym: xkb.Keysym,
-    release: bool,
-) ?usize {
-    for (mappings.items, 0..) |mapping, i| {
-        if (meta.eql(mapping.modifiers, modifiers) and
-            mapping.keysym == keysym and mapping.options.release == release)
-        {
-            return i;
-        }
-    }
-
-    return null;
-}
-
-/// Returns the index of the SwitchMapping with matching switch_type and switch_state, if any.
-fn switchMappingExists(
-    switch_mappings: *std.ArrayListUnmanaged(SwitchMapping),
-    switch_type: Switch.Type,
-    switch_state: Switch.State,
-) ?usize {
-    for (switch_mappings.items, 0..) |mapping, i| {
-        if (mapping.switch_type == switch_type and meta.eql(mapping.switch_state, switch_state)) {
-            return i;
-        }
-    }
-
-    return null;
-}
-
-/// Returns the index of the PointerMapping with matching modifiers and event code, if any.
-fn pointerMappingExists(
-    pointer_mappings: *std.ArrayListUnmanaged(PointerMapping),
-    modifiers: wlr.Keyboard.ModifierMask,
-    event_code: u32,
-) ?usize {
-    for (pointer_mappings.items, 0..) |mapping, i| {
-        if (meta.eql(mapping.modifiers, modifiers) and mapping.event_code == event_code) {
-            return i;
-        }
-    }
-
-    return null;
-}
-
-fn parseEventCode(name: [:0]const u8, out: *?[]const u8) !u32 {
-    const event_code = c.libevdev_event_code_from_name(c.EV_KEY, name.ptr);
-    if (event_code < 1) {
-        out.* = try fmt.allocPrint(util.gpa, "unknown button {s}", .{name});
-        return Error.Other;
-    }
-
-    return @intCast(event_code);
-}
-
-fn parseKeysym(name: [:0]const u8, out: *?[]const u8) !xkb.Keysym {
-    const keysym = xkb.Keysym.fromName(name, .case_insensitive);
-    if (keysym == .NoSymbol) {
-        out.* = try fmt.allocPrint(util.gpa, "invalid keysym '{s}'", .{name});
-        return Error.Other;
-    }
-
-    // The case insensitive matching done by xkbcommon returns the first
-    // lowercase match found if there are multiple matches that differ only in
-    // case. This works great for alphabetic keys for example but there is one
-    // problematic exception we handle specially here. For some reason there
-    // exist both uppercase and lowercase versions of XF86ScreenSaver with
-    // different keysym values for example. Switching to a case-sensitive match
-    // would be too much of a breaking change at this point so fix this by
-    // special-casing this exception.
-    //
-    // This has been fixed upstream in libxkbcommon 1.7.0
-    // https://github.com/xkbcommon/libxkbcommon/pull/465
-    // TODO remove the workaround once libxkbcommon 1.7.0 is widely distributed.
-    if (@intFromEnum(keysym) == xkb.Keysym.XF86Screensaver) {
-        if (mem.eql(u8, name, "XF86Screensaver")) {
-            return keysym;
-        } else if (mem.eql(u8, name, "XF86ScreenSaver")) {
-            return @enumFromInt(xkb.Keysym.XF86ScreenSaver);
-        } else {
-            out.* = try fmt.allocPrint(util.gpa, "ambiguous keysym name '{s}'", .{name});
-            return Error.Other;
-        }
-    }
-
-    return keysym;
-}
-
-fn parseModifiers(modifiers_str: []const u8, out: *?[]const u8) !wlr.Keyboard.ModifierMask {
-    var it = mem.split(u8, modifiers_str, "+");
-    var modifiers = wlr.Keyboard.ModifierMask{};
-    outer: while (it.next()) |mod_name| {
-        if (mem.eql(u8, mod_name, "None")) continue;
-        inline for ([_]struct { name: []const u8, field_name: []const u8 }{
-            .{ .name = "Shift", .field_name = "shift" },
-            .{ .name = "Control", .field_name = "ctrl" },
-            .{ .name = "Mod1", .field_name = "alt" },
-            .{ .name = "Alt", .field_name = "alt" },
-            .{ .name = "Mod3", .field_name = "mod3" },
-            .{ .name = "Mod4", .field_name = "logo" },
-            .{ .name = "Super", .field_name = "logo" },
-            .{ .name = "Mod5", .field_name = "mod5" },
-        }) |def| {
-            if (mem.eql(u8, def.name, mod_name)) {
-                @field(modifiers, def.field_name) = true;
-                continue :outer;
-            }
-        }
-        out.* = try fmt.allocPrint(util.gpa, "invalid modifier '{s}'", .{mod_name});
-        return Error.Other;
-    }
-    return modifiers;
-}
-
-fn parseSwitchType(
-    switch_type_str: []const u8,
-    out: *?[]const u8,
-) !Switch.Type {
-    return meta.stringToEnum(Switch.Type, switch_type_str) orelse {
-        out.* = try std.fmt.allocPrint(
-            util.gpa,
-            "invalid switch '{s}', must be 'lid' or 'tablet'",
-            .{switch_type_str},
-        );
-        return Error.Other;
-    };
-}
-
-fn parseSwitchState(
-    switch_type: Switch.Type,
-    switch_state_str: []const u8,
-    out: *?[]const u8,
-) !Switch.State {
-    switch (switch_type) {
-        .lid => {
-            const lid_state = meta.stringToEnum(
-                Switch.LidState,
-                switch_state_str,
-            ) orelse {
-                out.* = try std.fmt.allocPrint(
-                    util.gpa,
-                    "invalid lid state '{s}', must be 'close' or 'open'",
-                    .{switch_state_str},
-                );
-                return Error.Other;
-            };
-            return Switch.State{ .lid = lid_state };
-        },
-        .tablet => {
-            const tablet_state = meta.stringToEnum(
-                Switch.TabletState,
-                switch_state_str,
-            ) orelse {
-                out.* = try std.fmt.allocPrint(
-                    util.gpa,
-                    "invalid tablet state '{s}', must be 'on' or 'off'",
-                    .{switch_state_str},
-                );
-                return Error.Other;
-            };
-            return Switch.State{ .tablet = tablet_state };
-        },
-    }
-}
-
-/// Remove a mapping from a given mode
-///
-/// Example:
-/// unmap normal Mod4+Shift Return
-pub fn unmap(seat: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error!void {
-    const result = flags.parser([:0]const u8, &.{
-        .{ .name = "release", .kind = .boolean },
-    }).parse(args[1..]) catch {
-        return error.InvalidValue;
-    };
-    if (result.args.len < 3) return Error.NotEnoughArguments;
-    if (result.args.len > 3) return Error.TooManyArguments;
-
-    const mode_id = try modeNameToId(result.args[0], out);
-    const modifiers = try parseModifiers(result.args[1], out);
-    const keysym = try parseKeysym(result.args[2], out);
-
-    const mode_mappings = &server.config.modes.items[mode_id].mappings;
-    const mapping_idx = mappingExists(
-        mode_mappings,
-        modifiers,
-        keysym,
-        result.flags.release,
-    ) orelse return;
-
-    // Repeating mappings borrow the Mapping directly. To prevent a possible
-    // crash if the Mapping ArrayList is reallocated, stop any currently
-    // repeating mappings.
-    seat.clearRepeatingMapping();
-
-    var mapping = mode_mappings.swapRemove(mapping_idx);
-    mapping.deinit();
-}
-
-/// Remove a switch mapping from a given mode
-///
-/// Example:
-/// unmap-switch normal tablet on
-pub fn unmapSwitch(
-    _: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    if (args.len < 4) return Error.NotEnoughArguments;
-
-    const mode_id = try modeNameToId(args[1], out);
-    const switch_type = try parseSwitchType(args[2], out);
-    const switch_state = try parseSwitchState(switch_type, args[3], out);
-
-    const mode_mappings = &server.config.modes.items[mode_id].switch_mappings;
-    const mapping_idx = switchMappingExists(mode_mappings, switch_type, switch_state) orelse return;
-
-    var mapping = mode_mappings.swapRemove(mapping_idx);
-    mapping.deinit();
-}
-
-/// Remove a pointer mapping for a given mode
-///
-/// Example:
-/// unmap-pointer normal Mod4 BTN_LEFT
-pub fn unmapPointer(_: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error!void {
-    if (args.len < 4) return Error.NotEnoughArguments;
-    if (args.len > 4) return Error.TooManyArguments;
-
-    const mode_id = try modeNameToId(args[1], out);
-    const modifiers = try parseModifiers(args[2], out);
-    const event_code = try parseEventCode(args[3], out);
-
-    const mode_pointer_mappings = &server.config.modes.items[mode_id].pointer_mappings;
-    const mapping_idx = pointerMappingExists(mode_pointer_mappings, modifiers, event_code) orelse return;
-
-    var mapping = mode_pointer_mappings.swapRemove(mapping_idx);
-    mapping.deinit();
-}
diff --git a/river/command/move.zig b/river/command/move.zig
deleted file mode 100644
index 1d0ca00..0000000
--- a/river/command/move.zig
+++ /dev/null
@@ -1,150 +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, version 3.
-//
-// 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 math = std.math;
-
-const server = &@import("../main.zig").server;
-
-const Error = @import("../command.zig").Error;
-const PhysicalDirection = @import("../command.zig").PhysicalDirection;
-const Orientation = @import("../command.zig").Orientation;
-const Seat = @import("../Seat.zig");
-const View = @import("../View.zig");
-
-pub fn move(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 3) return Error.NotEnoughArguments;
-    if (args.len > 3) return Error.TooManyArguments;
-
-    const delta = try std.fmt.parseInt(i32, args[2], 10);
-    const direction = std.meta.stringToEnum(PhysicalDirection, args[1]) orelse
-        return Error.InvalidPhysicalDirection;
-
-    const view = getView(seat) orelse return;
-    switch (direction) {
-        .up => view.pending.move(0, -delta),
-        .down => view.pending.move(0, delta),
-        .left => view.pending.move(-delta, 0),
-        .right => view.pending.move(delta, 0),
-    }
-
-    apply(view);
-}
-
-pub fn snap(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    const direction = std.meta.stringToEnum(PhysicalDirection, args[1]) orelse
-        return Error.InvalidPhysicalDirection;
-
-    const view = getView(seat) orelse return;
-    const output = view.pending.output orelse return;
-    const border_width = server.config.border_width;
-    var output_width: i32 = undefined;
-    var output_height: i32 = undefined;
-    output.wlr_output.effectiveResolution(&output_width, &output_height);
-    switch (direction) {
-        .up => view.pending.box.y = border_width,
-        .down => view.pending.box.y = output_height - view.pending.box.height - border_width,
-        .left => view.pending.box.x = border_width,
-        .right => view.pending.box.x = output_width - view.pending.box.width - border_width,
-    }
-
-    apply(view);
-}
-
-pub fn resize(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 3) return Error.NotEnoughArguments;
-    if (args.len > 3) return Error.TooManyArguments;
-
-    const delta = try std.fmt.parseInt(i32, args[2], 10);
-    const orientation = std.meta.stringToEnum(Orientation, args[1]) orelse
-        return Error.InvalidOrientation;
-
-    const view = getView(seat) orelse return;
-    var output_width: c_int = math.maxInt(c_int);
-    var output_height: c_int = math.maxInt(c_int);
-    if (view.pending.output) |output| {
-        output.wlr_output.effectiveResolution(&output_width, &output_height);
-    }
-    switch (orientation) {
-        .horizontal => {
-            const prev_width = view.pending.box.width;
-            view.pending.box.width += delta;
-            view.applyConstraints(&view.pending.box);
-            // Get width difference after applying view constraints, so that the
-            // move reflects the actual size difference, but before applying the
-            // output size constraints, to allow growing a view even if it is
-            // up against an output edge.
-            const diff_width = prev_width - view.pending.box.width;
-            // Do not grow bigger than the output
-            view.pending.box.width = @min(
-                view.pending.box.width,
-                output_width - 2 * server.config.border_width,
-            );
-            view.pending.move(@divFloor(diff_width, 2), 0);
-        },
-        .vertical => {
-            const prev_height = view.pending.box.height;
-            view.pending.box.height += delta;
-            view.applyConstraints(&view.pending.box);
-            const diff_height = prev_height - view.pending.box.height;
-            // Do not grow bigger than the output
-            view.pending.box.height = @min(
-                view.pending.box.height,
-                output_height - 2 * server.config.border_width,
-            );
-            view.pending.move(0, @divFloor(diff_height, 2));
-        },
-    }
-
-    apply(view);
-}
-
-fn apply(view: *View) void {
-    // Set the view to floating but keep the position and dimensions, if their
-    // dimensions are set by a layout generator. If however the views are
-    // unarranged, leave them as non-floating so the next active layout can
-    // affect them.
-    if (view.pending.output == null or view.pending.output.?.layout != null) {
-        view.pending.float = true;
-    }
-
-    server.root.applyPending();
-}
-
-fn getView(seat: *Seat) ?*View {
-    if (seat.focused != .view) return null;
-    const view = seat.focused.view;
-
-    // Do not touch fullscreen views
-    if (view.pending.fullscreen) return null;
-
-    return view;
-}
diff --git a/river/command/output.zig b/river/command/output.zig
deleted file mode 100644
index 8f7cd0e..0000000
--- a/river/command/output.zig
+++ /dev/null
@@ -1,135 +0,0 @@
-// 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, version 3.
-//
-// 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 assert = std.debug.assert;
-const mem = std.mem;
-const wl = @import("wayland").server.wl;
-const wlr = @import("wlroots");
-const flags = @import("flags");
-
-const server = &@import("../main.zig").server;
-const util = @import("../util.zig");
-
-const Direction = @import("../command.zig").Direction;
-const PhysicalDirectionDirection = @import("../command.zig").PhysicalDirection;
-const Error = @import("../command.zig").Error;
-const Output = @import("../Output.zig");
-const Seat = @import("../Seat.zig");
-
-pub fn focusOutput(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    // If the fallback pseudo-output is focused, there are no other outputs to switch to
-    if (seat.focused_output == null) {
-        assert(server.root.active_outputs.empty());
-        return;
-    }
-
-    seat.focusOutput((try getOutput(seat, args[1])) orelse return);
-    server.root.applyPending();
-}
-
-pub fn sendToOutput(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    const result = flags.parser([:0]const u8, &.{
-        .{ .name = "current-tags", .kind = .boolean },
-    }).parse(args[1..]) catch {
-        return error.InvalidOption;
-    };
-    if (result.args.len < 1) return Error.NotEnoughArguments;
-    if (result.args.len > 1) return Error.TooManyArguments;
-
-    // If the fallback pseudo-output is focused, there is nowhere to send the view
-    if (seat.focused_output == null) {
-        assert(server.root.active_outputs.empty());
-        return;
-    }
-
-    if (seat.focused == .view) {
-        const destination_output = (try getOutput(seat, result.args[0])) orelse return;
-
-        // If the view is already on destination_output, do nothing
-        if (seat.focused.view.pending.output == destination_output) return;
-
-        if (result.flags.@"current-tags") {
-            seat.focused.view.pending.tags = destination_output.pending.tags;
-        }
-
-        seat.focused.view.setPendingOutput(destination_output);
-
-        // When explicitly sending a view to an output, the user likely
-        // does not expect a previously evacuated view moved back to a
-        // re-connecting output.
-        if (seat.focused.view.output_before_evac) |name| {
-            util.gpa.free(name);
-            seat.focused.view.output_before_evac = null;
-        }
-
-        server.root.applyPending();
-    }
-}
-
-/// Find an output adjacent to the currently focused based on either logical or
-/// spacial direction
-fn getOutput(seat: *Seat, str: []const u8) !?*Output {
-    if (std.meta.stringToEnum(Direction, str)) |direction| { // Logical direction
-        // Return the next/prev output in the list
-        var link = &seat.focused_output.?.active_link;
-        link = switch (direction) {
-            .next => link.next.?,
-            .previous => link.prev.?,
-        };
-        // Wrap around list head
-        if (link == &server.root.active_outputs.link) {
-            link = switch (direction) {
-                .next => link.next.?,
-                .previous => link.prev.?,
-            };
-        }
-        return @as(*Output, @fieldParentPtr("active_link", link));
-    } else if (std.meta.stringToEnum(wlr.OutputLayout.Direction, str)) |direction| { // Spacial direction
-        var focus_box: wlr.Box = undefined;
-        server.root.output_layout.getBox(seat.focused_output.?.wlr_output, &focus_box);
-        if (focus_box.empty()) return null;
-
-        const wlr_output = server.root.output_layout.adjacentOutput(
-            direction,
-            seat.focused_output.?.wlr_output,
-            @floatFromInt(focus_box.x + @divTrunc(focus_box.width, 2)),
-            @floatFromInt(focus_box.y + @divTrunc(focus_box.height, 2)),
-        ) orelse return null;
-        return @as(*Output, @ptrFromInt(wlr_output.data));
-    } else {
-        // Check if an output matches by name
-        var it = server.root.active_outputs.iterator(.forward);
-        while (it.next()) |output| {
-            if (mem.eql(u8, mem.sliceTo(output.wlr_output.name, 0), str)) {
-                return output;
-            }
-        }
-        return Error.InvalidOutputIndicator;
-    }
-}
diff --git a/river/command/rule.zig b/river/command/rule.zig
deleted file mode 100644
index 7703955..0000000
--- a/river/command/rule.zig
+++ /dev/null
@@ -1,265 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2023 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, version 3.
-//
-// 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 fmt = std.fmt;
-
-const globber = @import("globber");
-const flags = @import("flags");
-
-const server = &@import("../main.zig").server;
-const util = @import("../util.zig");
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-const View = @import("../View.zig");
-
-const Action = enum {
-    float,
-    @"no-float",
-    ssd,
-    csd,
-    tags,
-    output,
-    position,
-    dimensions,
-    fullscreen,
-    @"no-fullscreen",
-};
-
-pub fn ruleAdd(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void {
-    const result = flags.parser([:0]const u8, &.{
-        .{ .name = "app-id", .kind = .arg },
-        .{ .name = "title", .kind = .arg },
-    }).parse(args[1..]) catch {
-        return error.InvalidValue;
-    };
-
-    if (result.args.len < 1) return Error.NotEnoughArguments;
-
-    const action = std.meta.stringToEnum(Action, result.args[0]) orelse return Error.UnknownOption;
-
-    const positional_arguments_count: u8 = switch (action) {
-        .float, .@"no-float", .ssd, .csd, .fullscreen, .@"no-fullscreen" => 1,
-        .tags, .output => 2,
-        .position, .dimensions => 3,
-    };
-    if (result.args.len > positional_arguments_count) return Error.TooManyArguments;
-    if (result.args.len < positional_arguments_count) return Error.NotEnoughArguments;
-
-    const app_id_glob = result.flags.@"app-id" orelse "*";
-    const title_glob = result.flags.title orelse "*";
-
-    try globber.validate(app_id_glob);
-    try globber.validate(title_glob);
-
-    switch (action) {
-        .float, .@"no-float" => {
-            try server.config.rules.float.add(.{
-                .app_id_glob = app_id_glob,
-                .title_glob = title_glob,
-                .value = (action == .float),
-            });
-        },
-        .ssd, .csd => {
-            try server.config.rules.ssd.add(.{
-                .app_id_glob = app_id_glob,
-                .title_glob = title_glob,
-                .value = (action == .ssd),
-            });
-            apply_ssd_rules();
-            server.root.applyPending();
-        },
-        .tags => {
-            const tags = try fmt.parseInt(u32, result.args[1], 10);
-            try server.config.rules.tags.add(.{
-                .app_id_glob = app_id_glob,
-                .title_glob = title_glob,
-                .value = tags,
-            });
-        },
-        .output => {
-            const output_name = try util.gpa.dupe(u8, result.args[1]);
-            errdefer util.gpa.free(output_name);
-            try server.config.rules.output.add(.{
-                .app_id_glob = app_id_glob,
-                .title_glob = title_glob,
-                .value = output_name,
-            });
-        },
-        .position => {
-            const x = try fmt.parseInt(u31, result.args[1], 10);
-            const y = try fmt.parseInt(u31, result.args[2], 10);
-            try server.config.rules.position.add(.{
-                .app_id_glob = app_id_glob,
-                .title_glob = title_glob,
-                .value = .{
-                    .x = x,
-                    .y = y,
-                },
-            });
-        },
-        .dimensions => {
-            const width = try fmt.parseInt(u31, result.args[1], 10);
-            const height = try fmt.parseInt(u31, result.args[2], 10);
-            try server.config.rules.dimensions.add(.{
-                .app_id_glob = app_id_glob,
-                .title_glob = title_glob,
-                .value = .{
-                    .width = width,
-                    .height = height,
-                },
-            });
-        },
-        .fullscreen, .@"no-fullscreen" => {
-            try server.config.rules.fullscreen.add(.{
-                .app_id_glob = app_id_glob,
-                .title_glob = title_glob,
-                .value = (action == .fullscreen),
-            });
-        },
-    }
-}
-
-pub fn ruleDel(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void {
-    const result = flags.parser([:0]const u8, &.{
-        .{ .name = "app-id", .kind = .arg },
-        .{ .name = "title", .kind = .arg },
-    }).parse(args[1..]) catch {
-        return error.InvalidValue;
-    };
-
-    if (result.args.len > 1) return Error.TooManyArguments;
-    if (result.args.len < 1) return Error.NotEnoughArguments;
-
-    const action = std.meta.stringToEnum(Action, result.args[0]) orelse return Error.UnknownOption;
-
-    const rule = .{
-        .app_id_glob = result.flags.@"app-id" orelse "*",
-        .title_glob = result.flags.title orelse "*",
-    };
-    switch (action) {
-        .float, .@"no-float" => {
-            _ = server.config.rules.float.del(rule);
-        },
-        .ssd, .csd => {
-            _ = server.config.rules.ssd.del(rule);
-            apply_ssd_rules();
-            server.root.applyPending();
-        },
-        .tags => {
-            _ = server.config.rules.tags.del(rule);
-        },
-        .output => {
-            if (server.config.rules.output.del(rule)) |output_rule| {
-                util.gpa.free(output_rule);
-            }
-        },
-        .position => {
-            _ = server.config.rules.position.del(rule);
-        },
-        .dimensions => {
-            _ = server.config.rules.dimensions.del(rule);
-        },
-        .fullscreen, .@"no-fullscreen" => {
-            _ = server.config.rules.fullscreen.del(rule);
-        },
-    }
-}
-
-fn apply_ssd_rules() void {
-    var it = server.root.views.iterator(.forward);
-    while (it.next()) |view| {
-        if (view.destroying) continue;
-
-        if (server.config.rules.ssd.match(view)) |ssd| {
-            view.pending.ssd = ssd;
-        }
-    }
-}
-
-pub fn listRules(_: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error!void {
-    if (args.len < 2) return error.NotEnoughArguments;
-    if (args.len > 2) return error.TooManyArguments;
-
-    const rule_list = std.meta.stringToEnum(enum {
-        float,
-        ssd,
-        tags,
-        output,
-        position,
-        dimensions,
-        fullscreen,
-    }, args[1]) orelse return Error.UnknownOption;
-    const max_glob_len = switch (rule_list) {
-        inline else => |list| @field(server.config.rules, @tagName(list)).getMaxGlobLen(),
-    };
-    const app_id_column_max = 2 + @max("app-id".len, max_glob_len.app_id);
-    const title_column_max = 2 + @max("title".len, max_glob_len.title);
-
-    var buffer = std.ArrayList(u8).init(util.gpa);
-    const writer = buffer.writer();
-
-    try fmt.formatBuf("title", .{ .width = title_column_max, .alignment = .left }, writer);
-    try fmt.formatBuf("app-id", .{ .width = app_id_column_max, .alignment = .left }, writer);
-    try writer.writeAll("action\n");
-
-    switch (rule_list) {
-        inline .float, .ssd, .output, .fullscreen => |list| {
-            const rules = switch (list) {
-                .float => server.config.rules.float.rules.items,
-                .ssd => server.config.rules.ssd.rules.items,
-                .output => server.config.rules.output.rules.items,
-                .fullscreen => server.config.rules.fullscreen.rules.items,
-                else => unreachable,
-            };
-            for (rules) |rule| {
-                try fmt.formatBuf(rule.title_glob, .{ .width = title_column_max, .alignment = .left }, writer);
-                try fmt.formatBuf(rule.app_id_glob, .{ .width = app_id_column_max, .alignment = .left }, writer);
-                try writer.print("{s}\n", .{switch (list) {
-                    .float => if (rule.value) "float" else "no-float",
-                    .ssd => if (rule.value) "ssd" else "csd",
-                    .output => rule.value,
-                    .fullscreen => if (rule.value) "fullscreen" else "no-fullscreen",
-                    else => unreachable,
-                }});
-            }
-        },
-        .tags => {
-            for (server.config.rules.tags.rules.items) |rule| {
-                try fmt.formatBuf(rule.title_glob, .{ .width = title_column_max, .alignment = .left }, writer);
-                try fmt.formatBuf(rule.app_id_glob, .{ .width = app_id_column_max, .alignment = .left }, writer);
-                try writer.print("{b}\n", .{rule.value});
-            }
-        },
-        .position => {
-            for (server.config.rules.position.rules.items) |rule| {
-                try fmt.formatBuf(rule.title_glob, .{ .width = title_column_max, .alignment = .left }, writer);
-                try fmt.formatBuf(rule.app_id_glob, .{ .width = app_id_column_max, .alignment = .left }, writer);
-                try writer.print("{d},{d}\n", .{ rule.value.x, rule.value.y });
-            }
-        },
-        .dimensions => {
-            for (server.config.rules.dimensions.rules.items) |rule| {
-                try fmt.formatBuf(rule.title_glob, .{ .width = title_column_max, .alignment = .left }, writer);
-                try fmt.formatBuf(rule.app_id_glob, .{ .width = app_id_column_max, .alignment = .left }, writer);
-                try writer.print("{d}x{d}\n", .{ rule.value.width, rule.value.height });
-            }
-        },
-    }
-
-    out.* = try buffer.toOwnedSlice();
-}
diff --git a/river/command/set_repeat.zig b/river/command/set_repeat.zig
deleted file mode 100644
index e58908f..0000000
--- a/river/command/set_repeat.zig
+++ /dev/null
@@ -1,45 +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, version 3.
-//
-// 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 server = &@import("../main.zig").server;
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-/// Set the repeat rate and delay for all keyboards.
-pub fn setRepeat(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 3) return Error.NotEnoughArguments;
-    if (args.len > 3) return Error.TooManyArguments;
-
-    const rate = try std.fmt.parseInt(u31, args[1], 10);
-    const delay = try std.fmt.parseInt(u31, args[2], 10);
-
-    server.config.repeat_rate = rate;
-    server.config.repeat_delay = delay;
-
-    var it = server.input_manager.devices.iterator(.forward);
-    while (it.next()) |device| {
-        if (device.wlr_device.type == .keyboard) {
-            device.wlr_device.toKeyboard().setRepeatInfo(rate, delay);
-        }
-    }
-}
diff --git a/river/command/spawn.zig b/river/command/spawn.zig
deleted file mode 100644
index 030535a..0000000
--- a/river/command/spawn.zig
+++ /dev/null
@@ -1,62 +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, version 3.
-//
-// 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 posix = std.posix;
-
-const c = @import("../c.zig");
-const util = @import("../util.zig");
-const process = @import("../process.zig");
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-/// Spawn a program.
-pub fn spawn(
-    _: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    const child_args = [_:null]?[*:0]const u8{ "/bin/sh", "-c", args[1], null };
-
-    const pid = posix.fork() catch {
-        out.* = try std.fmt.allocPrint(util.gpa, "fork/execve failed", .{});
-        return Error.Other;
-    };
-
-    if (pid == 0) {
-        process.cleanupChild();
-
-        const pid2 = posix.fork() catch c._exit(1);
-        if (pid2 == 0) {
-            posix.execveZ("/bin/sh", &child_args, std.c.environ) catch c._exit(1);
-        }
-
-        c._exit(0);
-    }
-
-    // Wait the intermediate child.
-    const ret = posix.waitpid(pid, 0);
-    if (!posix.W.IFEXITED(ret.status) or
-        (posix.W.IFEXITED(ret.status) and posix.W.EXITSTATUS(ret.status) != 0))
-    {
-        out.* = try std.fmt.allocPrint(util.gpa, "fork/execve failed", .{});
-        return Error.Other;
-    }
-}
diff --git a/river/command/tags.zig b/river/command/tags.zig
deleted file mode 100644
index d1ac8da..0000000
--- a/river/command/tags.zig
+++ /dev/null
@@ -1,144 +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, version 3.
-//
-// 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 mem = std.mem;
-
-const server = &@import("../main.zig").server;
-const util = @import("../util.zig");
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-/// Switch focus to the passed tags.
-pub fn setFocusedTags(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    const tags = try parseTags(args, out);
-    const output = seat.focused_output orelse return;
-    if (output.pending.tags != tags) {
-        output.previous_tags = output.pending.tags;
-        output.pending.tags = tags;
-        server.root.applyPending();
-    }
-}
-
-pub fn spawnTagmask(
-    _: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    const tags = try parseTags(args, out);
-    server.config.spawn_tagmask = tags;
-}
-
-/// Set the tags of the focused view.
-pub fn setViewTags(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    const tags = try parseTags(args, out);
-    if (seat.focused == .view) {
-        const view = seat.focused.view;
-        view.pending.tags = tags;
-        server.root.applyPending();
-    }
-}
-
-/// Toggle focus of the passsed tags.
-pub fn toggleFocusedTags(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    const tags = try parseTags(args, out);
-    const output = seat.focused_output orelse return;
-    const new_focused_tags = output.pending.tags ^ tags;
-    if (new_focused_tags != 0) {
-        output.previous_tags = output.pending.tags;
-        output.pending.tags = new_focused_tags;
-        server.root.applyPending();
-    }
-}
-
-/// Toggle the passed tags of the focused view
-pub fn toggleViewTags(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    const tags = try parseTags(args, out);
-    if (seat.focused == .view) {
-        const new_tags = seat.focused.view.pending.tags ^ tags;
-        if (new_tags != 0) {
-            const view = seat.focused.view;
-            view.pending.tags = new_tags;
-            server.root.applyPending();
-        }
-    }
-}
-
-/// Switch focus to tags that were selected previously
-pub fn focusPreviousTags(
-    seat: *Seat,
-    args: []const []const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len > 1) return error.TooManyArguments;
-    const output = seat.focused_output orelse return;
-    const previous_tags = output.previous_tags;
-    if (output.pending.tags != previous_tags) {
-        output.previous_tags = output.pending.tags;
-        output.pending.tags = previous_tags;
-        server.root.applyPending();
-    }
-}
-
-/// Set the tags of the focused view to the tags that were selected previously
-pub fn sendToPreviousTags(
-    seat: *Seat,
-    args: []const []const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len > 1) return error.TooManyArguments;
-
-    const output = seat.focused_output orelse return;
-    if (seat.focused == .view) {
-        const view = seat.focused.view;
-        view.pending.tags = output.previous_tags;
-        server.root.applyPending();
-    }
-}
-
-fn parseTags(
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!u32 {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    const tags = try std.fmt.parseInt(u32, args[1], 10);
-
-    if (tags == 0) {
-        out.* = try std.fmt.allocPrint(util.gpa, "tags may not be 0", .{});
-        return Error.Other;
-    }
-
-    return tags;
-}
diff --git a/river/command/toggle_float.zig b/river/command/toggle_float.zig
deleted file mode 100644
index 5185ae4..0000000
--- a/river/command/toggle_float.zig
+++ /dev/null
@@ -1,47 +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, version 3.
-//
-// 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 server = &@import("../main.zig").server;
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-/// Make the focused view float or stop floating, depending on its current
-/// state.
-pub fn toggleFloat(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len > 1) return Error.TooManyArguments;
-
-    if (seat.focused == .view) {
-        const view = seat.focused.view;
-
-        // If views are unarranged, don't allow changing the views float status.
-        // It would just lead to confusing because this state would not be
-        // visible immediately, only after a layout is connected.
-        if (view.pending.output == null or view.pending.output.?.layout == null) return;
-
-        // Don't float fullscreen views
-        if (view.pending.fullscreen) return;
-
-        view.pending.float = !view.pending.float;
-        server.root.applyPending();
-    }
-}
diff --git a/river/command/toggle_fullscreen.zig b/river/command/toggle_fullscreen.zig
deleted file mode 100644
index 5dfef41..0000000
--- a/river/command/toggle_fullscreen.zig
+++ /dev/null
@@ -1,38 +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, version 3.
-//
-// 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 server = &@import("../main.zig").server;
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-/// Toggle fullscreen state of the currently focused view
-pub fn toggleFullscreen(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len > 1) return Error.TooManyArguments;
-
-    if (seat.focused == .view) {
-        const view = seat.focused.view;
-
-        view.pending.fullscreen = !view.pending.fullscreen;
-        server.root.applyPending();
-    }
-}
diff --git a/river/command/view_operations.zig b/river/command/view_operations.zig
deleted file mode 100644
index 4f1319b..0000000
--- a/river/command/view_operations.zig
+++ /dev/null
@@ -1,145 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 - 2023 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, version 3.
-//
-// 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 assert = std.debug.assert;
-const wlr = @import("wlroots");
-const flags = @import("flags");
-
-const server = &@import("../main.zig").server;
-
-const Direction = @import("../command.zig").Direction;
-const Error = @import("../command.zig").Error;
-const Output = @import("../Output.zig");
-const Seat = @import("../Seat.zig");
-const View = @import("../View.zig");
-const Vector = @import("../Vector.zig");
-
-/// Focus either the next or the previous visible view, depending on the enum
-/// passed. Does nothing if there are 1 or 0 views in the stack.
-pub fn focusView(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    const result = flags.parser([:0]const u8, &.{
-        .{ .name = "skip-floating", .kind = .boolean },
-    }).parse(args[1..]) catch {
-        return error.InvalidValue;
-    };
-    if (result.args.len < 1) return Error.NotEnoughArguments;
-    if (result.args.len > 1) return Error.TooManyArguments;
-
-    if (try getTarget(
-        seat,
-        result.args[0],
-        if (result.flags.@"skip-floating") .skip_float else .all,
-    )) |target| {
-        assert(!target.pending.fullscreen);
-        seat.focus(target);
-        server.root.applyPending();
-    }
-}
-
-/// Swap the currently focused view with either the view higher or lower in the visible stack
-pub fn swap(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    if (try getTarget(seat, args[1], .skip_float)) |target| {
-        assert(!target.pending.float);
-        assert(!target.pending.fullscreen);
-        seat.focused.view.pending_wm_stack_link.swapWith(&target.pending_wm_stack_link);
-        seat.cursor.may_need_warp = true;
-        server.root.applyPending();
-    }
-}
-
-const TargetMode = enum { all, skip_float };
-fn getTarget(seat: *Seat, direction_str: []const u8, target_mode: TargetMode) !?*View {
-    if (seat.focused != .view) return null;
-    if (seat.focused.view.pending.fullscreen) return null;
-    if (target_mode == .skip_float and seat.focused.view.pending.float) return null;
-    const output = seat.focused_output orelse return null;
-
-    // If no currently view is focused, focus the first in the stack.
-    if (seat.focused != .view) {
-        var it = output.pending.wm_stack.iterator(.forward);
-        return it.next();
-    }
-
-    // Logical direction, based on the view stack.
-    if (std.meta.stringToEnum(Direction, direction_str)) |direction| {
-        switch (direction) {
-            inline else => |dir| {
-                const it_dir = comptime switch (dir) {
-                    .next => .forward,
-                    .previous => .reverse,
-                };
-                var it = output.pending.wm_stack.iterator(it_dir);
-                while (it.next()) |view| {
-                    if (view == seat.focused.view) break;
-                } else {
-                    unreachable;
-                }
-
-                // Return the next view in the stack matching the tags if any.
-                while (it.next()) |view| {
-                    if (target_mode == .skip_float and view.pending.float) continue;
-                    if (output.pending.tags & view.pending.tags != 0) return view;
-                }
-
-                // Wrap and return the first view in the stack matching the tags if
-                // any is found before completing the loop back to the focused view.
-                while (it.next()) |view| {
-                    if (view == seat.focused.view) return null;
-                    if (target_mode == .skip_float and view.pending.float) continue;
-                    if (output.pending.tags & view.pending.tags != 0) return view;
-                }
-
-                unreachable;
-            },
-        }
-    }
-
-    // Spatial direction, based on view position.
-    if (std.meta.stringToEnum(wlr.OutputLayout.Direction, direction_str)) |direction| {
-        const focus_position = Vector.positionOfBox(seat.focused.view.current.box);
-        var target: ?*View = null;
-        var target_distance: usize = std.math.maxInt(usize);
-        var it = output.pending.wm_stack.iterator(.forward);
-        while (it.next()) |view| {
-            if (output.pending.tags & view.pending.tags == 0) continue;
-            if (target_mode == .skip_float and view.pending.float) continue;
-            if (view == seat.focused.view) continue;
-            const view_position = Vector.positionOfBox(view.current.box);
-            const position_diff = focus_position.diff(view_position);
-            if ((position_diff.direction() orelse continue) != direction) continue;
-            const distance = position_diff.length();
-            if (distance < target_distance) {
-                target = view;
-                target_distance = distance;
-            }
-        }
-        return target;
-    }
-
-    return Error.InvalidDirection;
-}
diff --git a/river/command/xcursor_theme.zig b/river/command/xcursor_theme.zig
deleted file mode 100644
index 0e1e065..0000000
--- a/river/command/xcursor_theme.zig
+++ /dev/null
@@ -1,34 +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, version 3.
-//
-// 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 Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-pub fn xcursorTheme(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 3) return Error.TooManyArguments;
-
-    const name = args[1];
-    const size = if (args.len == 3) try std.fmt.parseInt(u32, args[2], 10) else null;
-
-    try seat.cursor.setTheme(name, size);
-}
diff --git a/river/command/zoom.zig b/river/command/zoom.zig
deleted file mode 100644
index 7db3500..0000000
--- a/river/command/zoom.zig
+++ /dev/null
@@ -1,85 +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, version 3.
-//
-// 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 assert = std.debug.assert;
-
-const server = &@import("../main.zig").server;
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-const View = @import("../View.zig");
-
-/// Bump the focused view to the top of the stack. If the view on the top of
-/// the stack is focused, bump the second view to the top.
-pub fn zoom(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len > 1) return Error.TooManyArguments;
-
-    if (seat.focused != .view) return;
-    if (seat.focused.view.pending.float or seat.focused.view.pending.fullscreen) return;
-
-    const output = seat.focused_output orelse return;
-
-    const layout_first = blk: {
-        var it = output.pending.wm_stack.iterator(.forward);
-        while (it.next()) |view| {
-            if (view.pending.tags & output.pending.tags != 0 and !view.pending.float) break :blk view;
-        } else {
-            // If we are focusing a view that is not fullscreen or floating
-            // it must be visible and in the layout.
-            unreachable;
-        }
-    };
-
-    // If the first view that is part of the layout is focused, zoom
-    // the next view in the layout if any. Otherwise zoom the focused view.
-    const zoom_target = blk: {
-        if (seat.focused.view == layout_first) {
-            var it = output.pending.wm_stack.iterator(.forward);
-            while (it.next()) |view| {
-                if (view == seat.focused.view) break;
-            } else {
-                unreachable;
-            }
-
-            while (it.next()) |view| {
-                if (view.pending.tags & output.pending.tags != 0 and !view.pending.float) break :blk view;
-            } else {
-                break :blk null;
-            }
-        } else {
-            break :blk seat.focused.view;
-        }
-    };
-
-    if (zoom_target) |target| {
-        assert(!target.pending.float);
-        assert(!target.pending.fullscreen);
-
-        target.pending_wm_stack_link.remove();
-        output.pending.wm_stack.prepend(target);
-        seat.focus(target);
-        // Focus may not actually change here so seat.focus() may not automatically warp the cursor.
-        // Nevertheless, a cursor warp seems to be what users expect with `set-cursor-warp on-focus`
-        // configured, especially in combination with focus-follows-cursor.
-        seat.cursor.may_need_warp = true;
-        server.root.applyPending();
-    }
-}