Merge pull request #3254 from orestisf1993/issue-555

Multiple assignments of workspaces to outputs (#555)
This commit is contained in:
Ingo Bürk 2018-07-12 16:18:13 +02:00 committed by GitHub
commit a1351138d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 246 additions and 69 deletions

View File

@ -888,7 +888,7 @@ the second screen and so on).
*Syntax*: *Syntax*:
------------------------------------- -------------------------------------
workspace <workspace> output <output> workspace <workspace> output <output1> [output2]…
------------------------------------- -------------------------------------
The 'output' is the name of the RandR output you attach your screen to. On a The 'output' is the name of the RandR output you attach your screen to. On a
@ -907,12 +907,15 @@ monitor name is “Dell UP2414Q”.
entire monitor, i3 will still use the entire area of the containing monitor entire monitor, i3 will still use the entire area of the containing monitor
rather than that of just the output's.) rather than that of just the output's.)
You can specify multiple outputs. The first available will be used.
If you use named workspaces, they must be quoted: If you use named workspaces, they must be quoted:
*Examples*: *Examples*:
--------------------------- ---------------------------
workspace 1 output LVDS1 workspace 1 output LVDS1
workspace 5 output VGA1 workspace 2 output primary
workspace 5 output VGA1 LVDS1
workspace "2: vim" output VGA1 workspace "2: vim" output VGA1
--------------------------- ---------------------------

View File

@ -38,6 +38,13 @@ Con *get_existing_workspace_by_name(const char *name);
*/ */
Con *get_existing_workspace_by_num(int num); Con *get_existing_workspace_by_num(int num);
/**
* Returns true if the first output assigned to a workspace with the given
* workspace assignment is the same as the given output.
*
*/
bool output_triggers_assignment(Output *output, struct Workspace_Assignment *assignment);
/** /**
* Returns a pointer to the workspace with the given number (starting at 0), * Returns a pointer to the workspace with the given number (starting at 0),
* creating the workspace if necessary (by allocating the necessary amount of * creating the workspace if necessary (by allocating the necessary amount of

View File

@ -273,7 +273,7 @@ state WORKSPACE_OUTPUT:
-> WORKSPACE_OUTPUT_STR -> WORKSPACE_OUTPUT_STR
state WORKSPACE_OUTPUT_STR: state WORKSPACE_OUTPUT_STR:
output = word output = string
-> call cfg_workspace($workspace, $output) -> call cfg_workspace($workspace, $output)
# ipc-socket <path> # ipc-socket <path>

View File

@ -2019,6 +2019,9 @@ void cmd_rename_workspace(I3_CMD, const char *old_name, const char *new_name) {
LOG("Could not get output named \"%s\"\n", assignment->output); LOG("Could not get output named \"%s\"\n", assignment->output);
continue; continue;
} }
if (!output_triggers_assignment(target_output, assignment)) {
continue;
}
workspace_move_to_output(workspace, target_output); workspace_move_to_output(workspace, target_output);
break; break;

View File

@ -322,8 +322,8 @@ CFGFUN(show_marks, const char *value) {
config.show_marks = eval_boolstr(value); config.show_marks = eval_boolstr(value);
} }
CFGFUN(workspace, const char *workspace, const char *output) { CFGFUN(workspace, const char *workspace, const char *outputs) {
DLOG("Assigning workspace \"%s\" to output \"%s\"\n", workspace, output); DLOG("Assigning workspace \"%s\" to outputs \"%s\"\n", workspace, outputs);
/* Check for earlier assignments of the same workspace so that we /* Check for earlier assignments of the same workspace so that we
* dont have assignments of a single workspace to different * dont have assignments of a single workspace to different
* outputs */ * outputs */
@ -336,10 +336,16 @@ CFGFUN(workspace, const char *workspace, const char *output) {
} }
} }
char *buf = sstrdup(outputs);
char *output = strtok(buf, " ");
while (output != NULL) {
assignment = scalloc(1, sizeof(struct Workspace_Assignment)); assignment = scalloc(1, sizeof(struct Workspace_Assignment));
assignment->name = sstrdup(workspace); assignment->name = sstrdup(workspace);
assignment->output = sstrdup(output); assignment->output = sstrdup(output);
TAILQ_INSERT_TAIL(&ws_assignments, assignment, ws_assignments); TAILQ_INSERT_TAIL(&ws_assignments, assignment, ws_assignments);
output = strtok(NULL, " ");
}
free(buf);
} }
CFGFUN(ipc_socket, const char *path) { CFGFUN(ipc_socket, const char *path) {

View File

@ -424,9 +424,9 @@ void init_ws_for_output(Output *output, Con *content) {
/* go through all assignments and move the existing workspaces to this output */ /* go through all assignments and move the existing workspaces to this output */
struct Workspace_Assignment *assignment; struct Workspace_Assignment *assignment;
TAILQ_FOREACH(assignment, &ws_assignments, ws_assignments) { TAILQ_FOREACH(assignment, &ws_assignments, ws_assignments) {
if (strcmp(assignment->output, output_primary_name(output)) != 0) if (!output_triggers_assignment(output, assignment)) {
continue; continue;
}
Con *workspace = get_existing_workspace_by_name(assignment->name); Con *workspace = get_existing_workspace_by_name(assignment->name);
if (workspace == NULL) if (workspace == NULL)
continue; continue;
@ -501,8 +501,9 @@ void init_ws_for_output(Output *output, Con *content) {
/* otherwise, we create the first assigned ws for this output */ /* otherwise, we create the first assigned ws for this output */
TAILQ_FOREACH(assignment, &ws_assignments, ws_assignments) { TAILQ_FOREACH(assignment, &ws_assignments, ws_assignments) {
if (strcmp(assignment->output, output_primary_name(output)) != 0) if (!output_triggers_assignment(output, assignment)) {
continue; continue;
}
LOG("Initializing first assigned workspace \"%s\" for output \"%s\"\n", LOG("Initializing first assigned workspace \"%s\" for output \"%s\"\n",
assignment->name, assignment->output); assignment->name, assignment->output);

View File

@ -67,6 +67,52 @@ static void _workspace_apply_default_orientation(Con *ws) {
} }
} }
/*
* Returns the first output that is assigned to a workspace specified by the
* given name or number or NULL if no such output exists. If there is a
* workspace with a matching name and another workspace with a matching number,
* the output assigned to the first one is returned.
* The order of the 'ws_assignments' queue is respected: if multiple assignments
* match the specified workspace, the first one is returned.
* If 'name' is NULL it will be ignored.
* If 'parsed_num' is -1 it will be ignored.
*
*/
static Con *get_assigned_output(const char *name, long parsed_num) {
Con *output = NULL;
struct Workspace_Assignment *assignment;
TAILQ_FOREACH(assignment, &ws_assignments, ws_assignments) {
if (name && strcmp(assignment->name, name) == 0) {
DLOG("Found workspace name assignment to output \"%s\"\n", assignment->output);
Output *assigned_by_name = get_output_by_name(assignment->output, true);
if (assigned_by_name) {
/* When the name matches exactly, skip numbered assignments. */
return assigned_by_name->con;
}
} else if (!output && /* Only keep the first numbered assignment. */
parsed_num != -1 &&
name_is_digits(assignment->name) &&
ws_name_to_number(assignment->name) == parsed_num) {
DLOG("Found workspace number assignment to output \"%s\"\n", assignment->output);
Output *assigned_by_num = get_output_by_name(assignment->output, true);
if (assigned_by_num) {
output = assigned_by_num->con;
}
}
}
return output;
}
/*
* Returns true if the first output assigned to a workspace with the given
* workspace assignment is the same as the given output.
*/
bool output_triggers_assignment(Output *output, struct Workspace_Assignment *assignment) {
Con *assigned = get_assigned_output(assignment->name, -1);
return assigned && assigned == output->con;
}
/* /*
* Returns a pointer to the workspace with the given number (starting at 0), * Returns a pointer to the workspace with the given number (starting at 0),
* creating the workspace if necessary (by allocating the necessary amount of * creating the workspace if necessary (by allocating the necessary amount of
@ -78,25 +124,16 @@ Con *workspace_get(const char *num, bool *created) {
if (workspace == NULL) { if (workspace == NULL) {
LOG("Creating new workspace \"%s\"\n", num); LOG("Creating new workspace \"%s\"\n", num);
/* unless an assignment is found, we will create this workspace on the current output */
Con *output = con_get_output(focused);
/* look for assignments */
struct Workspace_Assignment *assignment;
/* We set workspace->num to the number if this workspaces name begins /* We set workspace->num to the number if this workspaces name begins
* with a positive number. Otherwise its a named ws and num will be * with a positive number. Otherwise its a named ws and num will be
* -1. */ * -1. */
long parsed_num = ws_name_to_number(num); long parsed_num = ws_name_to_number(num);
TAILQ_FOREACH(assignment, &ws_assignments, ws_assignments) { Con *output = get_assigned_output(num, parsed_num);
if (strcmp(assignment->name, num) == 0) { /* if an assignment is not found, we create this workspace on the current output */
DLOG("Found workspace name assignment to output \"%s\"\n", assignment->output); if (!output) {
GREP_FIRST(output, croot, !strcmp(child->name, assignment->output)); output = con_get_output(focused);
break;
} else if (parsed_num != -1 && name_is_digits(assignment->name) && ws_name_to_number(assignment->name) == parsed_num) {
DLOG("Found workspace number assignment to output \"%s\"\n", assignment->output);
GREP_FIRST(output, croot, !strcmp(child->name, assignment->output));
}
} }
Con *content = output_get_content(output); Con *content = output_get_content(output);
@ -208,20 +245,11 @@ Con *create_workspace_on_output(Output *output, Con *content) {
/* Ensure that this workspace is not assigned to a different output — /* Ensure that this workspace is not assigned to a different output —
* otherwise we would create it, then move it over to its output, then * otherwise we would create it, then move it over to its output, then
* find a new workspace, etc */ * find a new workspace, etc */
bool assigned = false; Con *assigned = get_assigned_output(target_name, -1);
struct Workspace_Assignment *assignment; if (assigned && assigned != output->con) {
TAILQ_FOREACH(assignment, &ws_assignments, ws_assignments) {
if (strcmp(assignment->name, target_name) != 0 ||
strcmp(assignment->output, output_primary_name(output)) == 0)
continue; continue;
assigned = true;
break;
} }
if (assigned)
continue;
exists = (get_existing_workspace_by_name(target_name) != NULL); exists = (get_existing_workspace_by_name(target_name) != NULL);
if (!exists) { if (!exists) {
ws->name = sstrdup(target_name); ws->name = sstrdup(target_name);
@ -239,7 +267,9 @@ Con *create_workspace_on_output(Output *output, Con *content) {
DLOG("Getting next unused workspace by number\n"); DLOG("Getting next unused workspace by number\n");
int c = 0; int c = 0;
while (exists) { while (exists) {
exists = (get_existing_workspace_by_num(++c) != NULL); c++;
Con *assigned = get_assigned_output(NULL, c);
exists = (get_existing_workspace_by_num(c) || (assigned && assigned != output->con));
DLOG("result for ws %d: exists = %d\n", c, exists); DLOG("result for ws %d: exists = %d\n", c, exists);
} }
ws->num = c; ws->num = c;
@ -946,8 +976,9 @@ bool workspace_move_to_output(Con *ws, Output *output) {
bool used_assignment = false; bool used_assignment = false;
struct Workspace_Assignment *assignment; struct Workspace_Assignment *assignment;
TAILQ_FOREACH(assignment, &ws_assignments, ws_assignments) { TAILQ_FOREACH(assignment, &ws_assignments, ws_assignments) {
if (assignment->output == NULL || strcmp(assignment->output, output_primary_name(current_output)) != 0) if (!output_triggers_assignment(current_output, assignment)) {
continue; continue;
}
/* check if this workspace is already attached to the tree */ /* check if this workspace is already attached to the tree */
if (get_existing_workspace_by_name(assignment->name) != NULL) { if (get_existing_workspace_by_name(assignment->name) != NULL) {
continue; continue;

View File

@ -26,6 +26,7 @@ use Data::Dumper ();
use Exporter (); use Exporter ();
our @EXPORT = qw( our @EXPORT = qw(
get_workspace_names get_workspace_names
get_output_for_workspace
get_unused_workspace get_unused_workspace
fresh_workspace fresh_workspace
get_ws_content get_ws_content
@ -402,6 +403,29 @@ sub get_workspace_names {
[ map { $_->{name} } @cons ] [ map { $_->{name} } @cons ]
} }
=head2 get_output_for_workspace()
Returns the name of the output on which this workspace resides
cmd 'focus output fake-1';
cmd 'workspace 1';
is(get_output_for_workspace('1', 'fake-0', 'Workspace 1 in output fake-0');
=cut
sub get_output_for_workspace {
my $ws_name = shift @_;
my $i3 = i3(get_socket_path());
my $tree = $i3->get_tree->recv;
my @outputs = @{$tree->{nodes}};
foreach (grep { not $_->{name} =~ /^__/ } @outputs) {
my $output = $_->{name};
foreach (grep { $_->{name} =~ "content" } @{$_->{nodes}}) {
return $output if $_->{nodes}[0]->{name} =~ $ws_name;
}
}
}
=head2 get_unused_workspace =head2 get_unused_workspace
Returns a workspace name which has not yet been used. See also Returns a workspace name which has not yet been used. See also

View File

@ -0,0 +1,102 @@
#!perl
# vim:ts=4:sw=4:expandtab
#
# Please read the following documents before working on tests:
# • https://build.i3wm.org/docs/testsuite.html
# (or docs/testsuite)
#
# • https://build.i3wm.org/docs/lib-i3test.html
# (alternatively: perldoc ./testcases/lib/i3test.pm)
#
# • https://build.i3wm.org/docs/ipc.html
# (or docs/ipc)
#
# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
# (unless you are already familiar with Perl)
#
# Test assignments of workspaces to outputs.
use i3test i3_autostart => 0;
################################################################################
# Test initial workspaces.
################################################################################
my $config = <<EOT;
font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
fake-outputs 1024x768+0+0,1024x768+1024+0,1024x768+1024+768,1024x768+0+768
bindsym Mod1+x workspace bindingname
workspace 9 output doesnotexist
workspace special output fake-0
workspace 1 output doesnotexist
workspace dontusethisname output doesnotexist
workspace donotoverride output fake-0
workspace 2 output fake-0
workspace 3 output fake-0
EOT
my $pid = launch_with_config($config);
sub check_output {
my ($workspace, $output, $msg) = @_;
is(get_output_for_workspace($workspace), $output, $msg);
}
check_output('9', '', 'Numbered workspace with a big number that is assigned to output that does not exist is not used');
check_output('special', 'fake-0', 'Output gets special workspace because of assignment');
check_output('bindingname', 'fake-1', 'Bindings give workspace names');
check_output('1', 'fake-2', 'Numbered workspace that is assigned to output that does not exist is used');
check_output('2', '', 'Numbered workspace assigned to output with existing workspace is not used');
check_output('3', '', 'Numbered workspace assigned to output with existing workspace is not used');
check_output('4', 'fake-3', 'First available number that is not assigned to existing output is used');
check_output('dontusethisname', '', 'Named workspace that is assigned to output that does not exist is not used');
check_output('donotoverride', '', 'Named workspace assigned to already occupied output is not used');
exit_gracefully($pid);
################################################################################
# Test multiple assignments
################################################################################
sub do_test {
my ($workspace, $output, $msg) = @_;
cmd 'focus output ' . ($output eq 'fake-3' ? 'fake-0' : 'fake-3');
cmd "workspace $workspace";
check_output($workspace, $output, $msg)
}
$config = <<EOT;
# i3 config file (v4)
font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
fake-outputs 1024x768+0+0P,1024x768+1024+0,1024x768+1024+768,1024x768+0+768
workspace 1 output fake-0
workspace 1 output fake-1 fake-2
workspace 2 output fake-3 fake-4 fake-0 fake-1
workspace 3 output these outputs do not exist but these do: fake-0 fake-3
workspace 4 output whitespace fake-0
workspace special output doesnotexist1 doesnotexist2 doesnotexist3
EOT
$pid = launch_with_config($config);
do_test('1', 'fake-0', 'Multiple assignments do not override a single one');
do_test('2', 'fake-3', 'First output out of multiple assignments is used');
do_test('3', 'fake-0', 'First existing output is used');
do_test('4', 'fake-0', 'Excessive whitespace is ok');
do_test('5', 'fake-1', 'Numbered initialization for fake-1');
do_test('6', 'fake-2', 'Numbered initialization for fake-2');
cmd 'focus output fake-0, workspace special';
check_output('special', 'fake-0', 'Workspace with only non-existing assigned outputs opened in current output.');
# Moving assigned workspaces.
cmd 'workspace 2, move workspace to output left';
check_output('2', 'fake-2', 'Moved assigned workspace up');
exit_gracefully($pid);
done_testing;

View File

@ -26,25 +26,12 @@ workspace 1:override output fake-0
workspace 2 output fake-0 workspace 2 output fake-0
workspace 1 output fake-1 workspace 1 output fake-1
workspace 2:override output fake-1 workspace 2:override output fake-1
workspace 3 output fake-0
workspace 3:override output doesnotexist fake-1
fake-outputs 1024x768+0+0,1024x768+1024+0 fake-outputs 1024x768+0+0,1024x768+1024+0
EOT EOT
my $i3 = i3(get_socket_path());
$i3->connect->recv;
# Returns the name of the output on which this workspace resides
sub get_output_for_workspace {
my $ws_name = shift @_;
foreach (grep { not $_->{name} =~ /^__/ } @{$i3->get_tree->recv->{nodes}}) {
my $output = $_->{name};
foreach (grep { $_->{name} =~ "content" } @{$_->{nodes}}) {
return $output if $_->{nodes}[0]->{name} =~ $ws_name;
}
}
}
################################################################################ ################################################################################
# Workspace assignments with bare numbers should be interpreted as `workspace # Workspace assignments with bare numbers should be interpreted as `workspace
# number` config directives. Any workspace beginning with that number should be # number` config directives. Any workspace beginning with that number should be
@ -69,4 +56,10 @@ is(get_output_for_workspace('1:override'), 'fake-0',
'Assignment rules should not be affected by the order assignments are declared') 'Assignment rules should not be affected by the order assignments are declared')
or diag 'Since workspace "1:override" is assigned by name to fake-0, it should open on fake-0'; or diag 'Since workspace "1:override" is assigned by name to fake-0, it should open on fake-0';
cmd 'focus output fake-1';
cmd 'workspace "3:override"';
is(get_output_for_workspace('3:override'), 'fake-1',
'Assignment rules should not be affected by multiple output assignments')
or diag 'Since workspace "3:override" is assigned by name to fake-1, it should open on fake-1';
done_testing; done_testing;

View File

@ -29,23 +29,10 @@ workspace 2 output fake-1
workspace 3:foo output fake-1 workspace 3:foo output fake-1
workspace baz output fake-1 workspace baz output fake-1
workspace 5 output left workspace 5 output left
workspace 6 output doesnotexist fake-0
workspace 7 output fake-1 fake-0
EOT EOT
my $i3 = i3(get_socket_path());
$i3->connect->recv;
# Returns the name of the output on which this workspace resides
sub get_output_for_workspace {
my $ws_name = shift @_;
foreach (grep { not $_->{name} =~ /^__/ } @{$i3->get_tree->recv->{nodes}}) {
my $output = $_->{name};
foreach (grep { $_->{name} =~ "content" } @{$_->{nodes}}) {
return $output if $_->{nodes}[0]->{name} =~ $ws_name;
}
}
}
########################################################################## ##########################################################################
# Renaming the workspace to an unassigned name does not move the workspace # Renaming the workspace to an unassigned name does not move the workspace
# (regression test) # (regression test)
@ -106,4 +93,24 @@ cmd 'rename workspace 5 to 2';
is(get_output_for_workspace('2'), 'fake-1', is(get_output_for_workspace('2'), 'fake-1',
'Renaming a workspace so that it moves to the focused output which contains only an empty workspace should replace the empty workspace'); 'Renaming a workspace so that it moves to the focused output which contains only an empty workspace should replace the empty workspace');
##########################################################################
# Renaming a workspace with multiple assignments, where the first output
# doesn't exist.
##########################################################################
cmd 'focus output fake-1';
cmd 'rename workspace to 6';
is(get_output_for_workspace('6'), 'fake-0',
'Renaming the workspace while first target output doesn\'t exist moves it to the second assigned output');
##########################################################################
# Renaming a workspace with multiple assignments, where both outputs exist
# moves it to the first output.
##########################################################################
cmd 'focus output fake-0';
cmd 'rename workspace to 7';
is(get_output_for_workspace('7'), 'fake-1',
'Renaming a workspace with multiple assignments, where both outputs exist moves it to the first output.');
done_testing; done_testing;