diff --git a/Makefile b/Makefile index 89647f6c..25da180e 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ include $(TOPDIR)/common.mk # Depend on the object files of all source-files in src/*.c and on all header files AUTOGENERATED:=src/cfgparse.tab.c src/cfgparse.yy.c src/cmdparse.tab.c src/cmdparse.yy.c -FILES:=src/ipc.c src/main.c src/log.c src/util.c src/tree.c src/xcb.c src/manage.c src/workspace.c src/x.c src/floating.c src/click.c src/config.c src/handlers.c src/randr.c src/xinerama.c src/con.c src/load_layout.c src/render.c src/window.c src/match.c src/xcursor.c src/resize.c src/sighandler.c +FILES:=src/ipc.c src/main.c src/log.c src/util.c src/tree.c src/xcb.c src/manage.c src/workspace.c src/x.c src/floating.c src/click.c src/config.c src/handlers.c src/randr.c src/xinerama.c src/con.c src/load_layout.c src/render.c src/window.c src/match.c src/xcursor.c src/resize.c src/sighandler.c src/move.c FILES:=$(FILES:.c=.o) HEADERS:=$(filter-out include/loglevels.h,$(wildcard include/*.h)) diff --git a/i3.config b/i3.config index 185c2462..786ee6a6 100644 --- a/i3.config +++ b/i3.config @@ -66,10 +66,10 @@ bindsym Mod1+Down next v bindsym Mod1+Up prev v # Move -bindsym Mod1+Shift+n move before h -bindsym Mod1+Shift+r move before v -bindsym Mod1+Shift+t move after v -bindsym Mod1+Shift+d move after h +bindsym Mod1+Shift+n move left +bindsym Mod1+Shift+r move down +bindsym Mod1+Shift+t move up +bindsym Mod1+Shift+d move right # alternatively, you can use the cursor keys: bindsym Mod1+Shift+Left move before h diff --git a/include/all.h b/include/all.h index 3cc28940..dd2b9365 100644 --- a/include/all.h +++ b/include/all.h @@ -54,5 +54,6 @@ #include "xcursor.h" #include "resize.h" #include "sighandler.h" +#include "move.h" #endif diff --git a/include/con.h b/include/con.h index de537218..d5dc74e7 100644 --- a/include/con.h +++ b/include/con.h @@ -42,6 +42,9 @@ Con *con_get_output(Con *con); */ Con *con_get_workspace(Con *con); + +Con *con_parent_with_orientation(Con *con, orientation_t orientation); + /** * Returns the first fullscreen node below this node. * diff --git a/include/move.h b/include/move.h new file mode 100644 index 00000000..d0c97014 --- /dev/null +++ b/include/move.h @@ -0,0 +1,15 @@ +/* + * vim:ts=4:sw=4:expandtab + */ + +#ifndef _MOVE_H +#define _MOVE_H + +/** + * Moves the current container in the given direction (TOK_LEFT, TOK_RIGHT, + * TOK_UP, TOK_DOWN from cmdparse.l) + * + */ +void tree_move(int direction); + +#endif diff --git a/include/tree.h b/include/tree.h index c93d4c22..40d9a541 100644 --- a/include/tree.h +++ b/include/tree.h @@ -65,13 +65,6 @@ void tree_close_con(); */ void tree_next(char way, orientation_t orientation); -/** - * Moves the current container in the given way (next/previous) and given - * orientation (horizontal/vertical). - * - */ -void tree_move(char way, orientation_t orientation); - /** * Closes the given container including all children * diff --git a/include/workspace.h b/include/workspace.h index f65f1494..b902f490 100644 --- a/include/workspace.h +++ b/include/workspace.h @@ -99,4 +99,6 @@ void workspace_map_clients(xcb_connection_t *conn, Workspace *ws); */ void workspace_update_urgent_flag(Con *ws); +void ws_force_orientation(Con *ws, orientation_t orientation); + #endif diff --git a/src/cmdparse.l b/src/cmdparse.l index d8320578..ebd466af 100644 --- a/src/cmdparse.l +++ b/src/cmdparse.l @@ -112,8 +112,6 @@ up { return TOK_UP; } down { return TOK_DOWN; } left { return TOK_LEFT; } right { return TOK_RIGHT; } -before { return TOK_BEFORE; } -after { return TOK_AFTER; } resize { return TOK_RESIZE; } shrink { return TOK_SHRINK; } grow { return TOK_GROW; } diff --git a/src/cmdparse.y b/src/cmdparse.y index 50738f70..d71773fe 100644 --- a/src/cmdparse.y +++ b/src/cmdparse.y @@ -134,8 +134,6 @@ char *parse_cmd(const char *new) { %token TOK_DOWN "down" %token TOK_LEFT "left" %token TOK_RIGHT "right" -%token TOK_AFTER "after" -%token TOK_BEFORE "before" %token TOK_RESTORE "restore" %token TOK_MARK "mark" %token TOK_RESIZE "resize" @@ -527,12 +525,10 @@ level_direction: ; move: - TOK_MOVE WHITESPACE before_after WHITESPACE direction + TOK_MOVE WHITESPACE direction { - printf("moving: %s and %c\n", ($3 == TOK_BEFORE ? "before" : "after"), $5); - /* TODO: change API for the next call, we need to convert in both directions while ideally - * we should not need any of both */ - tree_move(($3 == TOK_BEFORE ? 'p' : 'n'), ($5 == 'v' ? VERT : HORIZ)); + printf("moving in direction %d\n", $3); + tree_move($3); } | TOK_MOVE WHITESPACE TOK_WORKSPACE WHITESPACE STR { @@ -555,11 +551,6 @@ move: } ; -before_after: - TOK_BEFORE { $$ = TOK_BEFORE; } - | TOK_AFTER { $$ = TOK_AFTER; } - ; - restore: TOK_RESTORE WHITESPACE STR { diff --git a/src/con.c b/src/con.c index 65b97443..d36a0da4 100644 --- a/src/con.c +++ b/src/con.c @@ -230,6 +230,24 @@ Con *con_get_workspace(Con *con) { return result; } +Con *con_parent_with_orientation(Con *con, orientation_t orientation) { + DLOG("Searching for parent of Con %p with orientation %d\n", con, orientation); + Con *parent = con->parent; + if (parent->type == CT_FLOATING_CON) + return NULL; + while (con_orientation(parent) != orientation) { + DLOG("Need to go one level further up\n"); + parent = parent->parent; + /* Abort when we reach a floating con */ + if (parent && parent->type == CT_FLOATING_CON) + parent = NULL; + if (parent == NULL) + break; + } + DLOG("Result: %p\n", parent); + return parent; +} + /* * helper data structure for the breadth-first-search in * con_get_fullscreen_con() @@ -742,15 +760,44 @@ void con_set_layout(Con *con, int layout) { } static void con_on_remove_child(Con *con) { + DLOG("on_remove_child\n"); + /* Nothing to do for workspaces */ - if (con->type == CT_WORKSPACE) + if (con->type == CT_WORKSPACE || con->type == CT_OUTPUT || con->type == CT_ROOT) { + DLOG("not handling, type = %d\n", con->type); return; + } /* TODO: check if this container would swallow any other client and * don’t close it automatically. */ - DLOG("on_remove_child\n"); - if (con_num_children(con) == 0) { + int children = con_num_children(con); + if (children == 0) { DLOG("Container empty, closing\n"); tree_close(con, false, false); + return; + } + + /* If we did not close the container, check if we have only a single child left */ + if (children == 1) { + Con *child = TAILQ_FIRST(&(con->nodes_head)); + Con *parent = con->parent; + DLOG("Container has only one child, replacing con %p with child %p\n", con, child); + + /* TODO: refactor it into con_swap */ + TAILQ_REPLACE(&(parent->nodes_head), con, child, nodes); + TAILQ_REPLACE(&(parent->focus_head), con, child, focused); + if (focused == con) + focused = child; + child->parent = parent; + child->percent = 0.0; + con_fix_percent(parent); + + con->parent = NULL; + x_con_kill(con); + free(con->name); + TAILQ_REMOVE(&all_cons, con, all_cons); + free(con); + + return; } } diff --git a/src/move.c b/src/move.c new file mode 100644 index 00000000..2700f205 --- /dev/null +++ b/src/move.c @@ -0,0 +1,174 @@ +/* + * vim:ts=4:sw=4:expandtab + */ + +#include "all.h" +#include "cmdparse.tab.h" + +typedef enum { BEFORE, AFTER } position_t; + +/* + * This function detaches 'con' from its parent and inserts it either before or + * after 'target'. + * + */ +static void insert_con_into(Con *con, Con *target, position_t position) { + Con *parent = target->parent; + /* We need to preserve the old con->parent. While it might still be used to + * insert the entry before/after it, we call the on_remove_child callback + * afterwards which might then close the con if it is empty. */ + Con *old_parent = con->parent; + + con_detach(con); + con_fix_percent(con->parent); + + con->parent = parent; + + if (position == BEFORE) { + TAILQ_INSERT_BEFORE(target, con, nodes); + TAILQ_INSERT_HEAD(&(parent->focus_head), con, focused); + } else if (position == AFTER) { + TAILQ_INSERT_AFTER(&(parent->nodes_head), target, con, nodes); + TAILQ_INSERT_HEAD(&(parent->focus_head), con, focused); + } + + /* Pretend the con was just opened with regards to size percent values. + * Since the con is moved to a completely different con, the old value + * does not make sense anyways. */ + con->percent = 0.0; + con_fix_percent(parent); + + CALL(old_parent, on_remove_child); +} + +/* + * This function detaches 'con' from its parent and inserts it at the given + * workspace. + * + */ +static void attach_to_workspace(Con *con, Con *ws) { + con_detach(con); + con_fix_percent(con->parent); + + CALL(con->parent, on_remove_child); + + con->parent = ws; + + TAILQ_INSERT_TAIL(&(ws->nodes_head), con, nodes); + TAILQ_INSERT_TAIL(&(ws->focus_head), con, focused); + + /* Pretend the con was just opened with regards to size percent values. + * Since the con is moved to a completely different con, the old value + * does not make sense anyways. */ + con->percent = 0.0; + con_fix_percent(ws); +} + +/* + * Moves the current container in the given direction (TOK_LEFT, TOK_RIGHT, + * TOK_UP, TOK_DOWN from cmdparse.l) + * + */ +void tree_move(int direction) { + DLOG("Moving in direction %d\n", direction); + /* 1: get the first parent with the same orientation */ + Con *con = focused; + + if (con->type == CT_WORKSPACE) { + DLOG("Not moving workspace\n"); + return; + } + + if (con->parent->type == CT_WORKSPACE && con_num_children(con->parent) == 1) { + DLOG("This is the only con on this workspace, not doing anything\n"); + return; + } + + orientation_t o = (direction == TOK_LEFT || direction == TOK_RIGHT ? HORIZ : VERT); + + Con *same_orientation = con_parent_with_orientation(con, o); + /* There is no parent container with the same orientation */ + if (!same_orientation) { + if (con_is_floating(con)) { + /* this is a floating con, we just disable floating */ + floating_disable(con, true); + return; + } + if (con_inside_floating(con)) { + /* 'con' should be moved out of a floating container */ + DLOG("Inside floating, moving to workspace\n"); + attach_to_workspace(con, con_get_workspace(con)); + goto end; + } + DLOG("Force-changing orientation\n"); + ws_force_orientation(con_get_workspace(con), o); + same_orientation = con_parent_with_orientation(con, o); + } + + /* easy case: the move is within this container */ + if (same_orientation == con->parent) { + DLOG("We are in the same container\n"); + Con *swap; + /* TODO: TAILQ_SWAP? */ + if (direction == TOK_LEFT || direction == TOK_UP) { + if (!(swap = TAILQ_PREV(con, nodes_head, nodes))) + return; + + if (!con_is_leaf(swap)) { + insert_con_into(con, con_descend_focused(swap), AFTER); + goto end; + } + + /* the container right of the current one is a normal one. */ + con_detach(con); + TAILQ_INSERT_BEFORE(swap, con, nodes); + TAILQ_INSERT_HEAD(&(swap->parent->focus_head), con, focused); + } else { + if (!(swap = TAILQ_NEXT(con, nodes))) + return; + + if (!con_is_leaf(swap)) { + insert_con_into(con, con_descend_focused(swap), AFTER); + goto end; + } + + con_detach(con); + TAILQ_INSERT_AFTER(&(swap->parent->nodes_head), swap, con, nodes); + TAILQ_INSERT_HEAD(&(swap->parent->focus_head), con, focused); + } + DLOG("Swapped.\n"); + return; + } + + /* this time, we have to move to another container */ + /* This is the container *above* 'con' which is inside 'same_orientation' */ + Con *above = con; + while (above->parent != same_orientation) + above = above->parent; + + DLOG("above = %p\n", above); + Con *next; + position_t position; + if (direction == TOK_UP || direction == TOK_LEFT) { + position = BEFORE; + next = TAILQ_PREV(above, nodes_head, nodes); + } else if (direction == TOK_DOWN || direction == TOK_RIGHT) { + position = AFTER; + next = TAILQ_NEXT(above, nodes); + } + + /* special case: there is a split container in the direction we are moving + * to, so descend and append */ + if (next && !con_is_leaf(next)) + insert_con_into(con, con_descend_focused(next), AFTER); + else + insert_con_into(con, above, position); + +end: + /* We need to call con_focus() to fix the focus stack "above" the container + * we just inserted the focused container into (otherwise, the parent + * container(s) would still point to the old container(s)). */ + con_focus(con); + + tree_flatten(croot); +} diff --git a/src/tree.c b/src/tree.c index 77da3ac3..f2b1d90e 100644 --- a/src/tree.c +++ b/src/tree.c @@ -365,213 +365,6 @@ void tree_next(char way, orientation_t orientation) { con_focus(con_descend_focused(next)); } -/* - * Moves the current container in the given way (next/previous) and given - * orientation (horizontal/vertical). - * - */ -void tree_move(char way, orientation_t orientation) { - /* 1: get the first parent with the same orientation */ - Con *con = focused; - Con *parent = con->parent; - Con *old_parent = parent; - if (con->type == CT_WORKSPACE) - return; - DLOG("con = %p / %s\n", con, con->name); - bool level_changed = false; - while (con_orientation(parent) != orientation) { - DLOG("need to go one level further up\n"); - /* If the current parent is an output, we are at a workspace - * and the orientation still does not match. In this case, we split the - * workspace to have the same look & feel as in older i3 releases. */ - if (parent->type == CT_WORKSPACE) { - DLOG("Arrived at workspace (%p / %s)\n", parent, parent->name); - /* In case of moving a window out of a floating con, there might be - * not a single tiling container. Makes no sense to split then, so - * just use the workspace as target */ - if (TAILQ_EMPTY(&(parent->nodes_head))) - break; - - /* Check if there are any other cons at all. If not, there is no - * point in creating a new split con and changing workspace - * orientation. Instead, the operation is a no-op. */ - Con *child; - bool other_container = false; - TAILQ_FOREACH(child, &(parent->nodes_head), nodes) - if (child != con) - other_container = true; - - if (!other_container) { - DLOG("No other container found, we are not creating this split container.\n"); - return; - } - - /* 1: create a new split container */ - Con *new = con_new(NULL); - new->parent = parent; - - /* 2: copy layout and orientation from workspace */ - new->layout = parent->layout; - new->orientation = parent->orientation; - - Con *old_focused = TAILQ_FIRST(&(parent->focus_head)); - if (old_focused == TAILQ_END(&(parent->focus_head))) - old_focused = NULL; - - /* 3: move the existing cons of this workspace below the new con */ - DLOG("Moving cons\n"); - while (!TAILQ_EMPTY(&(parent->nodes_head))) { - child = TAILQ_FIRST(&(parent->nodes_head)); - con_detach(child); - con_attach(child, new, true); - } - - /* 4: switch workspace orientation */ - parent->orientation = orientation; - - /* 5: attach the new split container to the workspace */ - DLOG("Attaching new split to ws\n"); - con_attach(new, parent, false); - - /* 6: fix the percentages */ - con_fix_percent(parent); - - if (old_focused) - con_focus(old_focused); - - level_changed = true; - - break; - } - parent = parent->parent; - level_changed = true; - } - /* If we have no tiling cons (when moving a window out of a floating con to - * an otherwise empty workspace for example), we just attach the window to - * the workspace. */ - bool fix_percent = false; - if (TAILQ_EMPTY(&(parent->nodes_head))) { - con_detach(con); - con_fix_percent(con->parent); - con->parent = parent; - fix_percent = true; - - TAILQ_INSERT_HEAD(&(parent->nodes_head), con, nodes); - TAILQ_INSERT_HEAD(&(parent->focus_head), con, focused); - } else { - Con *current = NULL, *loop; - /* Get the first tiling container in focus stack */ - TAILQ_FOREACH(loop, &(parent->focus_head), focused) { - if (loop->type == CT_FLOATING_CON) - continue; - current = loop; - break; - } - assert(current != TAILQ_END(&(parent->focus_head))); - - /* 2: chose next (or previous) */ - Con *next = current; - if (way == 'n') { - LOG("i would insert it after %p / %s\n", next, next->name); - - /* Have a look at the next container: If there is no next container or - * if it is a leaf node, we move the con one left to it. However, - * for split containers, we descend into it. */ - next = TAILQ_NEXT(next, nodes); - if (next == TAILQ_END(&(next->parent->nodes_head))) { - if (con == current) - return; - next = current; - } else { - if (level_changed && con_is_leaf(next)) { - next = current; - } else { - /* if this is a split container, we need to go down */ - next = con_descend_focused(next); - } - } - - con_detach(con); - if (con->parent != next->parent) { - con_fix_percent(con->parent); - con->parent = next->parent; - fix_percent = true; - } - - CALL(con->parent, on_remove_child); - - TAILQ_INSERT_AFTER(&(next->parent->nodes_head), next, con, nodes); - TAILQ_INSERT_HEAD(&(next->parent->focus_head), con, focused); - /* TODO: don’t influence focus handling? */ - } else { - LOG("i would insert it before %p / %s\n", current, current->name); - bool gone_down = false; - next = TAILQ_PREV(next, nodes_head, nodes); - if (next == TAILQ_END(&(next->parent->nodes_head))) { - if (con == current) { - DLOG("Cannot move, no other container in that direction\n"); - return; - } - next = current; - } else { - if (level_changed && con_is_leaf(next)) { - next = current; - } else { - /* if this is a split container, we need to go down */ - while (!TAILQ_EMPTY(&(next->focus_head))) { - gone_down = true; - next = TAILQ_FIRST(&(next->focus_head)); - } - } - } - - DLOG("detaching con = %p / %s, next = %p / %s\n", - con, con->name, next, next->name); - con_detach(con); - if (con->parent != next->parent) { - DLOG("different parents. new parent = %p / %s\n", next->parent, next->parent->name); - con_fix_percent(con->parent); - con->parent = next->parent; - fix_percent = true; - } - - /* After going down in the tree, we insert the container *after* - * the currently focused one even though the command used "before". - * This is to keep the user experience clear, since the before/after - * only signifies the direction of the movement on top-level */ - if (gone_down) - TAILQ_INSERT_AFTER(&(next->parent->nodes_head), next, con, nodes); - else TAILQ_INSERT_BEFORE(next, con, nodes); - TAILQ_INSERT_HEAD(&(next->parent->focus_head), con, focused); - /* TODO: don’t influence focus handling? */ - } - } - - /* fix the percentages in the container we moved to */ - if (fix_percent) { - int children = con_num_children(con->parent); - if (children == 1) { - con->percent = 1.0; - } else { - con->percent = 1.0 / (children - 1); - con_fix_percent(con->parent); - } - } - - /* We need to call con_focus() to fix the focus stack "above" the container - * we just inserted the focused container into (otherwise, the parent - * container(s) would still point to the old container(s)). */ - con_focus(con); - - /* fix the percentages in the container we moved from */ - if (level_changed) - con_fix_percent(old_parent); - - CALL(old_parent, on_remove_child); - - tree_flatten(croot); -} - /* * tree_flatten() removes pairs of redundant split containers, e.g.: * [workspace, horizontal] diff --git a/src/workspace.c b/src/workspace.c index 55f7dd17..c17eb7ed 100644 --- a/src/workspace.c +++ b/src/workspace.c @@ -487,3 +487,36 @@ void workspace_update_urgent_flag(Con *ws) { if (old_flag != ws->urgent) ipc_send_event("workspace", I3_IPC_EVENT_WORKSPACE, "{\"change\":\"urgent\"}"); } + +void ws_force_orientation(Con *ws, orientation_t orientation) { + /* 1: create a new split container */ + Con *split = con_new(NULL); + split->parent = ws; + + /* 2: copy layout and orientation from workspace */ + split->layout = ws->layout; + split->orientation = ws->orientation; + + Con *old_focused = TAILQ_FIRST(&(ws->focus_head)); + + /* 3: move the existing cons of this workspace below the new con */ + DLOG("Moving cons\n"); + while (!TAILQ_EMPTY(&(ws->nodes_head))) { + Con *child = TAILQ_FIRST(&(ws->nodes_head)); + con_detach(child); + con_attach(child, split, true); + } + + /* 4: switch workspace orientation */ + ws->orientation = orientation; + + /* 5: attach the new split container to the workspace */ + DLOG("Attaching new split to ws\n"); + con_attach(split, ws, false); + + /* 6: fix the percentages */ + con_fix_percent(ws); + + if (old_focused) + con_focus(old_focused); +} diff --git a/testcases/t/24-move.t b/testcases/t/24-move.t index 5d6a96ef..e887b9f3 100644 --- a/testcases/t/24-move.t +++ b/testcases/t/24-move.t @@ -13,13 +13,13 @@ use X11::XCB qw(:all); my $i3 = i3("/tmp/nestedcons"); my $tmp = get_unused_workspace(); -$i3->command("workspace $tmp")->recv; +cmd "workspace $tmp"; ###################################################################### # 1) move a container which cannot be moved ###################################################################### -$i3->command('open')->recv; +cmd 'open'; my $old_content = get_ws_content($tmp); is(@{$old_content}, 1, 'one container on this workspace'); @@ -46,22 +46,22 @@ my $second = $content->[1]->{id}; is($content->[0]->{id}, $first, 'first container unmodified'); # Move the second container before the first one (→ swap them) -$i3->command('move before h')->recv; +$i3->command('move left')->recv; $content = get_ws_content($tmp); is($content->[0]->{id}, $second, 'first container modified'); # We should not be able to move any further -$i3->command('move before h')->recv; +$i3->command('move left')->recv; $content = get_ws_content($tmp); is($content->[0]->{id}, $second, 'first container unmodified'); # Now move in the other direction -$i3->command('move after h')->recv; +$i3->command('move right')->recv; $content = get_ws_content($tmp); is($content->[0]->{id}, $first, 'first container modified'); # We should not be able to move any further -$i3->command('move after h')->recv; +$i3->command('move right')->recv; $content = get_ws_content($tmp); is($content->[0]->{id}, $first, 'first container unmodified'); @@ -84,7 +84,7 @@ $content = get_ws_content($tmp); is(@{$content}, 3, 'three containers on this workspace'); my $third = $content->[2]->{id}; -$i3->command('move before h')->recv; +$i3->command('move left')->recv; $content = get_ws_content($tmp); is(@{$content}, 2, 'only two containers on this workspace'); my $nodes = $content->[1]->{nodes}; @@ -95,19 +95,21 @@ is($nodes->[1]->{id}, $third, 'third container on bottom'); # move it inside the split container ###################################################################### -$i3->command('move before v')->recv; +$i3->command('move up')->recv; $nodes = get_ws_content($tmp)->[1]->{nodes}; is($nodes->[0]->{id}, $third, 'third container on top'); is($nodes->[1]->{id}, $second, 'second container on bottom'); # move it outside again -$i3->command('move before h')->recv; +$i3->command('move left')->recv; $content = get_ws_content($tmp); is(@{$content}, 3, 'three nodes on this workspace'); -$i3->command('move after h')->recv; +# due to automatic flattening/cleanup, the remaining split container +# will be replaced by the con itself, so we will still have 3 nodes +$i3->command('move right')->recv; $content = get_ws_content($tmp); -is(@{$content}, 2, 'two nodes on this workspace'); +is(@{$content}, 3, 'two nodes on this workspace'); ###################################################################### # 4) We create two v-split containers on the workspace, then we move @@ -116,7 +118,7 @@ is(@{$content}, 2, 'two nodes on this workspace'); ###################################################################### my $otmp = get_unused_workspace(); -$i3->command("workspace $otmp")->recv; +cmd "workspace $otmp"; $i3->command("open")->recv; $i3->command("open")->recv; @@ -125,9 +127,9 @@ $i3->command("open")->recv; $i3->command("prev h")->recv; $i3->command("split v")->recv; $i3->command("open")->recv; -$i3->command("move after h")->recv; +$i3->command("move right")->recv; $i3->command("prev h")->recv; -$i3->command("move after h")->recv; +$i3->command("move right")->recv; $content = get_ws_content($otmp); is(@{$content}, 1, 'only one nodes on this workspace');