diff --git a/doc/riverctl.1.scd b/doc/riverctl.1.scd
index be665e1..3dc8330 100644
--- a/doc/riverctl.1.scd
+++ b/doc/riverctl.1.scd
@@ -32,8 +32,8 @@ over the Wayland protocol.
Add _app-id_ to the float filter list. Views with this _app-id_
will start floating.
-*focus-output* *next*|*previous*
- Focus the next or previous output.
+*focus-output* *next*|*previous*|*up*|*right*|*down*|*left*
+ Focus the next or previous output or the closest output in any direction.
*focus-view* *next*|*previous*
Focus the next or previous view in the stack.
@@ -50,8 +50,9 @@ over the Wayland protocol.
Snap the focused view to the specified screen edge. The view will
be set to floating.
-*send-to-output* *next*|*previous*
- Send the focused view to the next or the previous output.
+*send-to-output* *next*|*previous*|*up*|*right*|*down*|*left*
+ Send the focused view to the next or previous output or the closest
+ output in any direction.
*spawn* _shell_command_
Run _shell_command_ using _/bin/sh -c_. Put single quotes around
diff --git a/river/command.zig b/river/command.zig
index b9ef7c6..313e11b 100644
--- a/river/command.zig
+++ b/river/command.zig
@@ -55,7 +55,7 @@ const str_to_impl_fn = [_]struct {
.{ .name = "exit", .impl = @import("command/exit.zig").exit },
.{ .name = "float-filter-add", .impl = @import("command/filter.zig").floatFilterAdd },
.{ .name = "focus-follows-cursor", .impl = @import("command/focus_follows_cursor.zig").focusFollowsCursor },
- .{ .name = "focus-output", .impl = @import("command/focus_output.zig").focusOutput },
+ .{ .name = "focus-output", .impl = @import("command/output.zig").focusOutput },
.{ .name = "focus-view", .impl = @import("command/focus_view.zig").focusView },
.{ .name = "input", .impl = @import("command/input.zig").input },
.{ .name = "list-inputs", .impl = @import("command/input.zig").listInputs },
@@ -67,7 +67,7 @@ const str_to_impl_fn = [_]struct {
.{ .name = "opacity", .impl = @import("command/opacity.zig").opacity },
.{ .name = "output-layout", .impl = @import("command/layout.zig").outputLayout },
.{ .name = "resize", .impl = @import("command/move.zig").resize },
- .{ .name = "send-to-output", .impl = @import("command/send_to_output.zig").sendToOutput },
+ .{ .name = "send-to-output", .impl = @import("command/output.zig").sendToOutput },
.{ .name = "set-focused-tags", .impl = @import("command/tags.zig").setFocusedTags },
.{ .name = "set-layout-value", .impl = @import("command/layout.zig").setLayoutValue },
.{ .name = "set-repeat", .impl = @import("command/set_repeat.zig").setRepeat },
diff --git a/river/command/focus_output.zig b/river/command/focus_output.zig
deleted file mode 100644
index 6d0f088..0000000
--- a/river/command/focus_output.zig
+++ /dev/null
@@ -1,55 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-const std = @import("std");
-
-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");
-
-/// Focus either the next or the previous output, depending on the bool passed.
-/// Does nothing if there is only one output.
-pub fn focusOutput(
- allocator: *std.mem.Allocator,
- seat: *Seat,
- args: []const []const u8,
- out: *?[]const u8,
-) Error!void {
- if (args.len < 2) return Error.NotEnoughArguments;
- if (args.len > 2) return Error.TooManyArguments;
-
- const direction = std.meta.stringToEnum(Direction, args[1]) orelse return Error.InvalidDirection;
-
- // If the noop output is focused, there are no other outputs to switch to
- if (seat.focused_output == &server.root.noop_output) {
- std.debug.assert(server.root.outputs.len == 0);
- return;
- }
-
- // Focus the next/prev output in the list if there is one, else wrap
- const focused_node = @fieldParentPtr(std.TailQueue(Output).Node, "data", seat.focused_output);
- seat.focusOutput(switch (direction) {
- .next => if (focused_node.next) |node| &node.data else &server.root.outputs.first.?.data,
- .previous => if (focused_node.prev) |node| &node.data else &server.root.outputs.last.?.data,
- });
-
- seat.focus(null);
- server.root.startTransaction();
-}
diff --git a/river/command/output.zig b/river/command/output.zig
new file mode 100644
index 0000000..5535b88
--- /dev/null
+++ b/river/command/output.zig
@@ -0,0 +1,99 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2021 The River Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+const std = @import("std");
+
+const wlr = @import("wlroots");
+
+const server = &@import("../main.zig").server;
+
+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(
+ allocator: *std.mem.Allocator,
+ seat: *Seat,
+ args: []const []const u8,
+ out: *?[]const u8,
+) Error!void {
+ if (args.len < 2) return Error.NotEnoughArguments;
+ if (args.len > 2) return Error.TooManyArguments;
+
+ // If the noop output is focused, there are no other outputs to switch to
+ if (seat.focused_output == &server.root.noop_output) {
+ std.debug.assert(server.root.outputs.len == 0);
+ return;
+ }
+
+ seat.focusOutput((try getOutput(seat, args[1])) orelse return);
+ seat.focus(null);
+ server.root.startTransaction();
+}
+
+pub fn sendToOutput(
+ allocator: *std.mem.Allocator,
+ seat: *Seat,
+ args: []const []const u8,
+ out: *?[]const u8,
+) Error!void {
+ if (args.len < 2) return Error.NotEnoughArguments;
+ if (args.len > 2) return Error.TooManyArguments;
+
+ // If the noop output is focused, there is nowhere to send the view
+ if (seat.focused_output == &server.root.noop_output) {
+ std.debug.assert(server.root.outputs.len == 0);
+ return;
+ }
+
+ if (seat.focused == .view) {
+ const destination_output = (try getOutput(seat, args[1])) orelse return;
+ seat.focused.view.sendToOutput(destination_output);
+
+ // Handle the change and focus whatever's next in the focus stack
+ seat.focus(null);
+ seat.focused_output.arrangeViews();
+ destination_output.arrangeViews();
+ server.root.startTransaction();
+ }
+}
+
+/// 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 if there is one, else wrap
+ const focused_node = @fieldParentPtr(std.TailQueue(Output).Node, "data", seat.focused_output);
+ return switch (direction) {
+ .next => if (focused_node.next) |node| &node.data else &server.root.outputs.first.?.data,
+ .previous => if (focused_node.prev) |node| &node.data else &server.root.outputs.last.?.data,
+ };
+ } else if (std.meta.stringToEnum(wlr.OutputLayout.Direction, str)) |direction| { // Spacial direction
+ const focus_box = server.root.output_layout.getBox(seat.focused_output.wlr_output) orelse return null;
+ const wlr_output = server.root.output_layout.adjacentOutput(
+ direction,
+ seat.focused_output.wlr_output,
+ @intToFloat(f64, focus_box.x + @divFloor(focus_box.width, 2)),
+ @intToFloat(f64, focus_box.y + @divFloor(focus_box.height, 2)),
+ ) orelse return null;
+ return @intToPtr(*Output, wlr_output.data);
+ } else {
+ return Error.InvalidDirection;
+ }
+}
diff --git a/river/command/send_to_output.zig b/river/command/send_to_output.zig
deleted file mode 100644
index e3ba90b..0000000
--- a/river/command/send_to_output.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, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-const std = @import("std");
-
-const 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");
-
-/// Send the focused view to the the next or the previous output, depending on
-/// the bool passed. Does nothing if there is only one output.
-pub fn sendToOutput(
- allocator: *std.mem.Allocator,
- seat: *Seat,
- args: []const []const u8,
- out: *?[]const u8,
-) Error!void {
- if (args.len < 2) return Error.NotEnoughArguments;
- if (args.len > 2) return Error.TooManyArguments;
-
- const direction = std.meta.stringToEnum(Direction, args[1]) orelse return Error.InvalidDirection;
-
- if (seat.focused == .view) {
- // If the noop output is focused, there is nowhere to send the view
- if (seat.focused_output == &server.root.noop_output) {
- std.debug.assert(server.root.outputs.len == 0);
- return;
- }
-
- // Send to the next/prev output in the list if there is one, else wrap
- const current_node = @fieldParentPtr(std.TailQueue(Output).Node, "data", seat.focused_output);
- const destination_output = switch (direction) {
- .next => if (current_node.next) |node| &node.data else &server.root.outputs.first.?.data,
- .previous => if (current_node.prev) |node| &node.data else &server.root.outputs.last.?.data,
- };
-
- // Move the view to the target output
- seat.focused.view.sendToOutput(destination_output);
-
- // Handle the change and focus whatever's next in the focus stack
- seat.focus(null);
- seat.focused_output.arrangeViews();
- destination_output.arrangeViews();
- server.root.startTransaction();
- }
-}