commands: accept 'next'/'prev' output names

`move container to output`, `move workspace to output` and `focus
output` now accept 'next' and 'prev', which cycle through the enabled
outputs with wrap-around. This matches i3's behaviour of expanding
'next' into the full output list so the following output relative to
the current one is picked.

The name/direction/current resolution used by the move commands was
also duplicated inline in `focus_output`; both call sites now share a
new `output_by_direction_or_name` helper in `sway/desktop/output.c`.

Closes: #8799

i3 implementation in https://github.com/i3/i3/pull/4338
Note:
- i3 accepts multiple output arguments and `next` works as a wildcard. I
  considered that out of scope for this PR.
- i3 does not have `prev`. Accepting it in sway is natural and
  practically free complexity-wise so I added it.
This commit is contained in:
Orestis Floros 2026-04-26 08:55:33 +02:00
parent 1cbb8a440f
commit 9b81b6273f
No known key found for this signature in database
GPG key ID: A09DBD7D3222C1C3
5 changed files with 114 additions and 93 deletions

View file

@ -110,6 +110,9 @@ struct sway_output *output_by_name_or_id(const char *name_or_id);
// this includes all the outputs, including disabled ones // this includes all the outputs, including disabled ones
struct sway_output *all_output_by_name_or_id(const char *name_or_id); struct sway_output *all_output_by_name_or_id(const char *name_or_id);
struct sway_output *output_by_direction_or_name(const char *spec,
const struct sway_output *reference, double ref_lx, double ref_ly);
void output_sort_workspaces(struct sway_output *output); void output_sort_workspaces(struct sway_output *output);
void output_enable(struct sway_output *output); void output_enable(struct sway_output *output);

View file

@ -1,6 +1,5 @@
#include <float.h> #include <float.h>
#include <strings.h> #include <strings.h>
#include <wlr/types/wlr_output_layout.h>
#include "log.h" #include "log.h"
#include "sway/commands.h" #include "sway/commands.h"
#include "sway/input/input-manager.h" #include "sway/input/input-manager.h"
@ -12,7 +11,6 @@
#include "sway/tree/view.h" #include "sway/tree/view.h"
#include "sway/tree/workspace.h" #include "sway/tree/workspace.h"
#include "stringop.h" #include "stringop.h"
#include "util.h"
static bool get_direction_from_next_prev(struct sway_container *container, static bool get_direction_from_next_prev(struct sway_container *container,
struct sway_seat *seat, const char *name, enum wlr_direction *out) { struct sway_seat *seat, const char *name, enum wlr_direction *out) {
@ -295,43 +293,31 @@ static struct cmd_results *focus_output(struct sway_seat *seat,
int argc, char **argv) { int argc, char **argv) {
if (!argc) { if (!argc) {
return cmd_results_new(CMD_INVALID, return cmd_results_new(CMD_INVALID,
"Expected 'focus output <direction|name>'."); "Expected 'focus output <direction|name|next|prev|current>'.");
} }
char *identifier = join_args(argv, argc); char *identifier = join_args(argv, argc);
struct sway_output *output = output_by_name_or_id(identifier);
struct sway_workspace *ws = seat_get_focused_workspace(seat);
struct sway_output *reference = ws ? ws->output : NULL;
double ref_lx = 0, ref_ly = 0;
if (reference) {
ref_lx = reference->lx + reference->width / 2;
ref_ly = reference->ly + reference->height / 2;
}
struct sway_output *output = output_by_direction_or_name(identifier,
reference, ref_lx, ref_ly);
if (!output) { if (!output) {
enum wlr_direction direction; struct cmd_results *err = cmd_results_new(CMD_INVALID,
if (!parse_direction(identifier, &direction)) { "No output matches '%s'.", identifier);
free(identifier); free(identifier);
return cmd_results_new(CMD_INVALID, return err;
"There is no output with that name.");
}
struct sway_workspace *ws = seat_get_focused_workspace(seat);
if (!ws) {
free(identifier);
return cmd_results_new(CMD_FAILURE,
"No focused workspace to base directions off of.");
}
output = output_get_in_direction(ws->output, direction);
if (!output) {
int center_lx = ws->output->lx + ws->output->width / 2;
int center_ly = ws->output->ly + ws->output->height / 2;
struct wlr_output *target = wlr_output_layout_farthest_output(
root->output_layout, opposite_direction(direction),
ws->output->wlr_output, center_lx, center_ly);
if (target) {
output = output_from_wlr_output(target);
}
}
} }
free(identifier); free(identifier);
if (output) {
seat_set_focus(seat, seat_get_focus_inactive(seat, &output->node)); seat_set_focus(seat, seat_get_focus_inactive(seat, &output->node));
seat_consider_warp_to_focus(seat); seat_consider_warp_to_focus(seat);
}
return cmd_results_new(CMD_SUCCESS, NULL); return cmd_results_new(CMD_SUCCESS, NULL);
} }
@ -426,7 +412,7 @@ struct cmd_results *cmd_focus(int argc, char **argv) {
if (!get_direction_from_next_prev(container, seat, argv[0], &direction)) { if (!get_direction_from_next_prev(container, seat, argv[0], &direction)) {
return cmd_results_new(CMD_INVALID, return cmd_results_new(CMD_INVALID,
"Expected 'focus <direction|next|prev|parent|child|mode_toggle|floating|tiling>' " "Expected 'focus <direction|next|prev|parent|child|mode_toggle|floating|tiling>' "
"or 'focus output <direction|name>'"); "or 'focus output <direction|name|next|prev|current>'");
} else if (argc == 2 && strcasecmp(argv[1], "sibling") == 0) { } else if (argc == 2 && strcasecmp(argv[1], "sibling") == 0) {
descend = false; descend = false;
} }

View file

@ -24,58 +24,9 @@
static const char expected_syntax[] = static const char expected_syntax[] =
"Expected 'move <left|right|up|down> <[px] px>' or " "Expected 'move <left|right|up|down> <[px] px>' or "
"'move [--no-auto-back-and-forth] <container|window> [to] workspace <name>' or " "'move [--no-auto-back-and-forth] <container|window> [to] workspace <name>' or "
"'move <container|window|workspace> [to] output <name|direction>' or " "'move <container|window|workspace> [to] output <name|direction|next|prev|current>' or "
"'move <container|window> [to] mark <mark>'"; "'move <container|window> [to] mark <mark>'";
static struct sway_output *output_in_direction(const char *direction_string,
struct sway_output *reference, int ref_lx, int ref_ly) {
if (strcasecmp(direction_string, "current") == 0) {
struct sway_workspace *active_ws =
seat_get_focused_workspace(config->handler_context.seat);
if (!active_ws) {
return NULL;
}
return active_ws->output;
}
struct {
char *name;
enum wlr_direction direction;
} names[] = {
{ "up", WLR_DIRECTION_UP },
{ "down", WLR_DIRECTION_DOWN },
{ "left", WLR_DIRECTION_LEFT },
{ "right", WLR_DIRECTION_RIGHT },
};
enum wlr_direction direction = 0;
for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); ++i) {
if (strcasecmp(names[i].name, direction_string) == 0) {
direction = names[i].direction;
break;
}
}
if (reference && direction) {
struct wlr_output *target = wlr_output_layout_adjacent_output(
root->output_layout, direction, reference->wlr_output,
ref_lx, ref_ly);
if (!target) {
target = wlr_output_layout_farthest_output(
root->output_layout, opposite_direction(direction),
reference->wlr_output, ref_lx, ref_ly);
}
if (target) {
return target->data;
}
}
return output_by_name_or_id(direction_string);
}
static bool is_parallel(enum sway_container_layout layout, static bool is_parallel(enum sway_container_layout layout,
enum wlr_direction dir) { enum wlr_direction dir) {
switch (layout) { switch (layout) {
@ -516,7 +467,7 @@ static struct cmd_results *cmd_move_container(bool no_auto_back_and_forth,
struct sway_container *dst = seat_get_focus_inactive_tiling(seat, ws); struct sway_container *dst = seat_get_focus_inactive_tiling(seat, ws);
destination = dst ? &dst->node : &ws->node; destination = dst ? &dst->node : &ws->node;
} else if (strcasecmp(argv[0], "output") == 0) { } else if (strcasecmp(argv[0], "output") == 0) {
struct sway_output *new_output = output_in_direction(argv[1], struct sway_output *new_output = output_by_direction_or_name(argv[1],
old_output, container->pending.x, container->pending.y); old_output, container->pending.x, container->pending.y);
if (!new_output) { if (!new_output) {
return cmd_results_new(CMD_FAILURE, return cmd_results_new(CMD_FAILURE,
@ -650,7 +601,7 @@ static struct cmd_results *cmd_move_workspace(int argc, char **argv) {
struct sway_output *old_output = workspace->output; struct sway_output *old_output = workspace->output;
int center_x = workspace->width / 2 + workspace->x, int center_x = workspace->width / 2 + workspace->x,
center_y = workspace->height / 2 + workspace->y; center_y = workspace->height / 2 + workspace->y;
struct sway_output *new_output = output_in_direction(argv[0], struct sway_output *new_output = output_by_direction_or_name(argv[0],
old_output, center_x, center_y); old_output, center_x, center_y);
if (!new_output) { if (!new_output) {
return cmd_results_new(CMD_FAILURE, return cmd_results_new(CMD_FAILURE,

View file

@ -7,29 +7,22 @@
#include <wlr/backend/headless.h> #include <wlr/backend/headless.h>
#include <wlr/render/swapchain.h> #include <wlr/render/swapchain.h>
#include <wlr/render/wlr_renderer.h> #include <wlr/render/wlr_renderer.h>
#include <wlr/types/wlr_buffer.h>
#include <wlr/types/wlr_alpha_modifier_v1.h> #include <wlr/types/wlr_alpha_modifier_v1.h>
#include <wlr/types/wlr_gamma_control_v1.h>
#include <wlr/types/wlr_output_layout.h> #include <wlr/types/wlr_output_layout.h>
#include <wlr/types/wlr_output_management_v1.h> #include <wlr/types/wlr_output_management_v1.h>
#include <wlr/types/wlr_output_power_management_v1.h> #include <wlr/types/wlr_output_power_management_v1.h>
#include <wlr/types/wlr_output.h> #include <wlr/types/wlr_output.h>
#include <wlr/types/wlr_presentation_time.h> #include <wlr/types/wlr_presentation_time.h>
#include <wlr/types/wlr_compositor.h> #include <wlr/types/wlr_compositor.h>
#include <wlr/util/region.h>
#include <wlr/util/transform.h>
#include "config.h"
#include "log.h" #include "log.h"
#include "sway/config.h" #include "sway/config.h"
#include "sway/desktop/transaction.h" #include "sway/desktop/transaction.h"
#include "sway/input/input-manager.h" #include "sway/input/input-manager.h"
#include "sway/input/seat.h" #include "sway/input/seat.h"
#include "sway/ipc-server.h" #include "sway/ipc-server.h"
#include "sway/layers.h"
#include "sway/output.h" #include "sway/output.h"
#include "sway/scene_descriptor.h" #include "sway/scene_descriptor.h"
#include "sway/server.h" #include "sway/server.h"
#include "sway/tree/arrange.h"
#include "sway/tree/container.h" #include "sway/tree/container.h"
#include "sway/tree/root.h" #include "sway/tree/root.h"
#include "sway/tree/view.h" #include "sway/tree/view.h"
@ -72,6 +65,75 @@ struct sway_output *all_output_by_name_or_id(const char *name_or_id) {
return NULL; return NULL;
} }
static struct sway_output *output_cycle(const struct sway_output *reference,
bool forward) {
if (root->outputs->length == 0) {
return NULL;
}
int ref_idx = -1;
if (reference) {
for (int i = 0; i < root->outputs->length; ++i) {
if (root->outputs->items[i] == reference) {
ref_idx = i;
break;
}
}
}
int next_idx;
if (ref_idx < 0) {
next_idx = forward ? 0 : root->outputs->length - 1;
} else {
next_idx = forward
? (ref_idx + 1) % root->outputs->length
: (ref_idx - 1 + root->outputs->length) % root->outputs->length;
}
return root->outputs->items[next_idx];
}
struct sway_output *output_by_direction_or_name(const char *spec,
const struct sway_output *reference, double ref_lx, double ref_ly) {
if (strcasecmp(spec, "current") == 0) {
const struct sway_workspace *active_ws =
seat_get_focused_workspace(config->handler_context.seat);
return active_ws ? active_ws->output : NULL;
}
if (strcasecmp(spec, "next") == 0) {
return output_cycle(reference, true);
}
if (strcasecmp(spec, "prev") == 0) {
return output_cycle(reference, false);
}
enum wlr_direction direction = 0;
if (strcasecmp(spec, "up") == 0) {
direction = WLR_DIRECTION_UP;
} else if (strcasecmp(spec, "down") == 0) {
direction = WLR_DIRECTION_DOWN;
} else if (strcasecmp(spec, "left") == 0) {
direction = WLR_DIRECTION_LEFT;
} else if (strcasecmp(spec, "right") == 0) {
direction = WLR_DIRECTION_RIGHT;
}
if (reference && direction) {
struct wlr_output *target = wlr_output_layout_adjacent_output(
root->output_layout, direction, reference->wlr_output,
ref_lx, ref_ly);
if (!target) {
target = wlr_output_layout_farthest_output(
root->output_layout, opposite_direction(direction),
reference->wlr_output, ref_lx, ref_ly);
}
if (target) {
return target->data;
}
}
return output_by_name_or_id(spec);
}
struct sway_workspace *output_get_active_workspace(struct sway_output *output) { struct sway_workspace *output_get_active_workspace(struct sway_output *output) {
struct sway_seat *seat = input_manager_current_seat(); struct sway_seat *seat = input_manager_current_seat();

View file

@ -139,6 +139,13 @@ They are expected to be used with *bindsym* or at runtime through *swaymsg*(1).
*focus* output up|right|down|left *focus* output up|right|down|left
Moves focus to the next output in the specified direction. Moves focus to the next output in the specified direction.
*focus* output next|prev
Moves focus to the next or previous output, wrapping around.
*focus* output current
Moves focus to the output containing the focused workspace. Mainly useful
with criteria to refocus the seat's current output.
*focus* output <name> *focus* output <name>
Moves focus to the named output. Moves focus to the named output.
@ -275,6 +282,10 @@ set|plus|minus|toggle <amount>
Moves the focused container to next output in the specified Moves the focused container to next output in the specified
direction. direction.
*move* [container|window] [to] output next|prev
Moves the focused container to the next or previous output,
wrapping around.
*move* [container|window] [to] scratchpad *move* [container|window] [to] scratchpad
Moves the focused container to the scratchpad. Moves the focused container to the scratchpad.
@ -290,6 +301,14 @@ set|plus|minus|toggle <amount>
*move* workspace to [output] up|right|down|left *move* workspace to [output] up|right|down|left
Moves the focused workspace to next output in the specified direction. Moves the focused workspace to next output in the specified direction.
*move* workspace [to] output next|prev
Moves the focused workspace to the next or previous output,
wrapping around.
*move* workspace to [output] next|prev
Moves the focused workspace to the next or previous output,
wrapping around.
*nop* <comment> *nop* <comment>
A no operation command that can be used to override default behaviour. The A no operation command that can be used to override default behaviour. The
optional comment argument is ignored, but logged for debugging purposes. optional comment argument is ignored, but logged for debugging purposes.