Merge pull request #2018 from Airblader/feature-2014

Allow multiple marks on a window
This commit is contained in:
Michael Stapelberg 2015-10-22 20:32:05 +02:00
commit 5333d15180
15 changed files with 344 additions and 98 deletions

View File

@ -1659,10 +1659,13 @@ workspace::
the special value +\_\_focused__+ to match all windows in the currently
focused workspace.
con_mark::
Compares the mark set for this container, see <<vim_like_marks>>.
Compares the marks set for this container, see <<vim_like_marks>>. A
match is made if any of the container's marks matches the specified
mark.
con_id::
Compares the i3-internal container ID, which you can get via the IPC
interface. Handy for scripting.
interface. Handy for scripting. Use the special value +\_\_focused__+
to match only the currently focused window.
The criteria +class+, +instance+, +role+, +title+, +workspace+ and +mark+ are
actually regular expressions (PCRE). See +pcresyntax(3)+ or +perldoc perlre+ for
@ -2113,24 +2116,36 @@ for this purpose: It lets you input a command and sends the command to i3. It
can also prefix this command and display a custom prompt for the input dialog.
The additional +--toggle+ option will remove the mark if the window already has
this mark, add it if the window has none or replace the current mark if it has
another mark.
this mark or add it otherwise. Note that you may need to use this in
combination with +--add+ (see below) as any other marks will otherwise be
removed.
By default, a window can only have one mark. You can use the +--add+ flag to
put more than one mark on a window.
Refer to <<show_marks>> if you don't want marks to be shown in the window decoration.
*Syntax*:
------------------------------
mark [--toggle] <identifier>
----------------------------------------------
mark [--add|--replace] [--toggle] <identifier>
[con_mark="identifier"] focus
unmark <identifier>
------------------------------
----------------------------------------------
*Example (in a terminal)*:
------------------------------
$ i3-msg mark irssi
$ i3-msg '[con_mark="irssi"] focus'
$ i3-msg unmark irssi
------------------------------
---------------------------------------------------------
# marks the focused container
mark irssi
# focus the container with the mark "irssi"
'[con_mark="irssi"] focus'
# remove the mark "irssi" from whichever container has it
unmark irssi
# remove all marks on all firefox windows
[class="(?i)firefox"] unmark
---------------------------------------------------------
///////////////////////////////////////////////////////////////////
TODO: make i3-input replace %s

View File

@ -115,10 +115,10 @@ void cmd_workspace_back_and_forth(I3_CMD);
void cmd_workspace_name(I3_CMD, const char *name);
/**
* Implementation of 'mark [--toggle] <mark>'
* Implementation of 'mark [--add|--replace] [--toggle] <mark>'
*
*/
void cmd_mark(I3_CMD, const char *mark, const char *toggle);
void cmd_mark(I3_CMD, const char *mark, const char *mode, const char *toggle);
/**
* Implementation of 'unmark [mark]'

View File

@ -146,26 +146,34 @@ Con *con_by_frame_id(xcb_window_t frame);
*/
Con *con_by_mark(const char *mark);
/**
* Returns true if and only if the given containers holds the mark.
*
*/
bool con_has_mark(Con *con, const char *mark);
/**
* Toggles the mark on a container.
* If the container already has this mark, the mark is removed.
* Otherwise, the mark is assigned to the container.
*
*/
void con_mark_toggle(Con *con, const char *mark);
void con_mark_toggle(Con *con, const char *mark, mark_mode_t mode);
/**
* Assigns a mark to the container.
*
*/
void con_mark(Con *con, const char *mark);
void con_mark(Con *con, const char *mark, mark_mode_t mode);
/**
* If mark is NULL, this removes all existing marks.
/*
* Removes marks from containers.
* If con is NULL, all containers are considered.
* If name is NULL, this removes all existing marks.
* Otherwise, it will only remove the given mark (if it is present).
*
*/
void con_unmark(const char *mark);
void con_unmark(Con *con, const char *name);
/**
* Returns the first container below 'con' which wants to swallow this window

View File

@ -46,6 +46,7 @@ typedef struct Con Con;
typedef struct Match Match;
typedef struct Assignment Assignment;
typedef struct Window i3Window;
typedef struct mark_t mark_t;
/******************************************************************************
* Helper types
@ -74,6 +75,9 @@ typedef enum { ADJ_NONE = 0,
ADJ_UPPER_SCREEN_EDGE = (1 << 2),
ADJ_LOWER_SCREEN_EDGE = (1 << 4) } adjacent_t;
typedef enum { MM_REPLACE,
MM_ADD } mark_mode_t;
/**
* Container layouts. See Con::layout.
*/
@ -523,6 +527,12 @@ typedef enum { CF_NONE = 0,
CF_OUTPUT = 1,
CF_GLOBAL = 2 } fullscreen_mode_t;
struct mark_t {
char *name;
TAILQ_ENTRY(mark_t) marks;
};
/**
* A 'Con' represents everything from the X11 root window down to a single X11 window.
*
@ -575,8 +585,8 @@ struct Con {
* displayed on whichever of the containers is currently visible */
char *sticky_group;
/* user-definable mark to jump to this container later */
char *mark;
/* user-definable marks to jump to this container later */
TAILQ_HEAD(marks_head, mark_t) marks_head;
/* cached to decide whether a redraw is needed */
bool mark_changed;

View File

@ -199,12 +199,14 @@ state FLOATING:
floating = 'enable', 'disable', 'toggle'
-> call cmd_floating($floating)
# mark [--toggle] <mark>
# mark [--add|--replace] [--toggle] <mark>
state MARK:
mode = '--add', '--replace'
->
toggle = '--toggle'
->
mark = string
-> call cmd_mark($mark, $toggle)
-> call cmd_mark($mark, $mode, $toggle)
# unmark [mark]
state UNMARK:

View File

@ -290,10 +290,16 @@ void cmd_criteria_match_windows(I3_CMD) {
DLOG("doesnt match\n");
free(current);
}
} else if (current_match->mark != NULL && current->con->mark != NULL &&
regex_matches(current_match->mark, current->con->mark)) {
DLOG("match by mark\n");
TAILQ_INSERT_TAIL(&owindows, current, owindows);
} else if (current_match->mark != NULL && !TAILQ_EMPTY(&(current->con->marks_head))) {
mark_t *mark;
TAILQ_FOREACH(mark, &(current->con->marks_head), marks) {
if (!regex_matches(current_match->mark, mark->name))
continue;
DLOG("match by mark\n");
TAILQ_INSERT_TAIL(&owindows, current, owindows);
break;
}
} else {
if (current->con->window && match_matches_window(current_match, current->con->window)) {
DLOG("matches window!\n");
@ -997,10 +1003,10 @@ void cmd_workspace_name(I3_CMD, const char *name) {
}
/*
* Implementation of 'mark [--toggle] <mark>'
* Implementation of 'mark [--add|--replace] [--toggle] <mark>'
*
*/
void cmd_mark(I3_CMD, const char *mark, const char *toggle) {
void cmd_mark(I3_CMD, const char *mark, const char *mode, const char *toggle) {
HANDLE_EMPTY_MATCH;
owindow *current = TAILQ_FIRST(&owindows);
@ -1016,10 +1022,12 @@ void cmd_mark(I3_CMD, const char *mark, const char *toggle) {
}
DLOG("matching: %p / %s\n", current->con, current->con->name);
mark_mode_t mark_mode = (mode == NULL || strcmp(mode, "--replace") == 0) ? MM_REPLACE : MM_ADD;
if (toggle != NULL) {
con_mark_toggle(current->con, mark);
con_mark_toggle(current->con, mark, mark_mode);
} else {
con_mark(current->con, mark);
con_mark(current->con, mark, mark_mode);
}
cmd_output->needs_tree_render = true;
@ -1032,7 +1040,14 @@ void cmd_mark(I3_CMD, const char *mark, const char *toggle) {
*
*/
void cmd_unmark(I3_CMD, const char *mark) {
con_unmark(mark);
if (match_is_empty(current_match)) {
con_unmark(NULL, mark);
} else {
owindow *current;
TAILQ_FOREACH(current, &owindows, owindows) {
con_unmark(current->con, mark);
}
}
cmd_output->needs_tree_render = true;
// XXX: default reply for now, make this a better reply

102
src/con.c
View File

@ -55,6 +55,7 @@ Con *con_new_skeleton(Con *parent, i3Window *window) {
TAILQ_INIT(&(new->nodes_head));
TAILQ_INIT(&(new->focus_head));
TAILQ_INIT(&(new->swallow_head));
TAILQ_INIT(&(new->marks_head));
if (parent != NULL)
con_attach(new, parent, false);
@ -512,27 +513,41 @@ Con *con_by_frame_id(xcb_window_t frame) {
Con *con_by_mark(const char *mark) {
Con *con;
TAILQ_FOREACH(con, &all_cons, all_cons) {
if (con->mark != NULL && strcmp(con->mark, mark) == 0)
if (con_has_mark(con, mark))
return con;
}
return NULL;
}
/*
* Returns true if and only if the given containers holds the mark.
*
*/
bool con_has_mark(Con *con, const char *mark) {
mark_t *current;
TAILQ_FOREACH(current, &(con->marks_head), marks) {
if (strcmp(current->name, mark) == 0)
return true;
}
return false;
}
/*
* Toggles the mark on a container.
* If the container already has this mark, the mark is removed.
* Otherwise, the mark is assigned to the container.
*
*/
void con_mark_toggle(Con *con, const char *mark) {
void con_mark_toggle(Con *con, const char *mark, mark_mode_t mode) {
assert(con != NULL);
DLOG("Toggling mark \"%s\" on con = %p.\n", mark, con);
if (con->mark != NULL && strcmp(con->mark, mark) == 0) {
con_unmark(mark);
if (con_has_mark(con, mark)) {
con_unmark(con, mark);
} else {
con_mark(con, mark);
con_mark(con, mark, mode);
}
}
@ -540,55 +555,76 @@ void con_mark_toggle(Con *con, const char *mark) {
* Assigns a mark to the container.
*
*/
void con_mark(Con *con, const char *mark) {
void con_mark(Con *con, const char *mark, mark_mode_t mode) {
assert(con != NULL);
DLOG("Setting mark \"%s\" on con = %p.\n", mark, con);
FREE(con->mark);
con->mark = sstrdup(mark);
con->mark_changed = true;
con_unmark(NULL, mark);
if (mode == MM_REPLACE) {
DLOG("Removing all existing marks on con = %p.\n", con);
DLOG("Clearing the mark from all other windows.\n");
Con *other;
TAILQ_FOREACH(other, &all_cons, all_cons) {
/* Skip the window we actually handled since we took care of it already. */
if (con == other)
continue;
if (other->mark != NULL && strcmp(other->mark, mark) == 0) {
FREE(other->mark);
other->mark_changed = true;
mark_t *current;
TAILQ_FOREACH(current, &(con->marks_head), marks) {
con_unmark(con, current->name);
}
}
mark_t *new = scalloc(1, sizeof(mark_t));
new->name = sstrdup(mark);
TAILQ_INSERT_TAIL(&(con->marks_head), new, marks);
con->mark_changed = true;
}
/*
* If mark is NULL, this removes all existing marks.
* Removes marks from containers.
* If con is NULL, all containers are considered.
* If name is NULL, this removes all existing marks.
* Otherwise, it will only remove the given mark (if it is present).
*
*/
void con_unmark(const char *mark) {
Con *con;
if (mark == NULL) {
void con_unmark(Con *con, const char *name) {
Con *current;
if (name == NULL) {
DLOG("Unmarking all containers.\n");
TAILQ_FOREACH(con, &all_cons, all_cons) {
if (con->mark == NULL)
TAILQ_FOREACH(current, &all_cons, all_cons) {
if (con != NULL && current != con)
continue;
FREE(con->mark);
con->mark_changed = true;
if (TAILQ_EMPTY(&(current->marks_head)))
continue;
mark_t *mark;
while (!TAILQ_EMPTY(&(current->marks_head))) {
mark = TAILQ_FIRST(&(current->marks_head));
FREE(mark->name);
TAILQ_REMOVE(&(current->marks_head), mark, marks);
FREE(mark);
}
current->mark_changed = true;
}
} else {
DLOG("Removing mark \"%s\".\n", mark);
con = con_by_mark(mark);
if (con == NULL) {
DLOG("Removing mark \"%s\".\n", name);
current = (con == NULL) ? con_by_mark(name) : con;
if (current == NULL) {
DLOG("No container found with this mark, so there is nothing to do.\n");
return;
}
DLOG("Found mark on con = %p. Removing it now.\n", con);
FREE(con->mark);
con->mark_changed = true;
DLOG("Found mark on con = %p. Removing it now.\n", current);
current->mark_changed = true;
mark_t *mark;
TAILQ_FOREACH(mark, &(current->marks_head), marks) {
if (strcmp(mark->name, name) != 0)
continue;
FREE(mark->name);
TAILQ_REMOVE(&(current->marks_head), mark, marks);
FREE(mark);
break;
}
}
}

View File

@ -275,9 +275,16 @@ void dump_node(yajl_gen gen, struct Con *con, bool inplace_restart) {
ystr("urgent");
y(bool, con->urgent);
if (con->mark != NULL) {
ystr("mark");
ystr(con->mark);
if (!TAILQ_EMPTY(&(con->marks_head))) {
ystr("marks");
y(array_open);
mark_t *mark;
TAILQ_FOREACH(mark, &(con->marks_head), marks) {
ystr(mark->name);
}
y(array_close);
}
ystr("focused");
@ -819,9 +826,12 @@ IPC_HANDLER(get_marks) {
y(array_open);
Con *con;
TAILQ_FOREACH(con, &all_cons, all_cons)
if (con->mark != NULL)
ystr(con->mark);
TAILQ_FOREACH(con, &all_cons, all_cons) {
mark_t *mark;
TAILQ_FOREACH(mark, &(con->marks_head), marks) {
ystr(mark->name);
}
}
y(array_close);

View File

@ -28,6 +28,7 @@ static bool parsing_deco_rect;
static bool parsing_window_rect;
static bool parsing_geometry;
static bool parsing_focus;
static bool parsing_marks;
struct Match *current_swallow;
/* This list is used for reordering the focus stack after parsing the 'focus'
@ -159,12 +160,16 @@ static int json_end_map(void *ctx) {
static int json_end_array(void *ctx) {
LOG("end of array\n");
if (!parsing_swallows && !parsing_focus) {
if (!parsing_swallows && !parsing_focus && !parsing_marks) {
con_fix_percent(json_node);
}
if (parsing_swallows) {
parsing_swallows = false;
}
if (parsing_marks) {
parsing_marks = false;
}
if (parsing_focus) {
/* Clear the list of focus mappings */
struct focus_mapping *mapping;
@ -214,6 +219,9 @@ static int json_key(void *ctx, const unsigned char *val, size_t len) {
if (strcasecmp(last_key, "focus") == 0)
parsing_focus = true;
if (strcasecmp(last_key, "marks") == 0)
parsing_marks = true;
return 1;
}
@ -234,6 +242,11 @@ static int json_string(void *ctx, const unsigned char *val, size_t len) {
ELOG("swallow key %s unknown\n", last_key);
}
free(sval);
} else if (parsing_marks) {
char *mark;
sasprintf(&mark, "%.*s", (int)len, val);
con_mark(json_node, mark, MM_ADD);
} else {
if (strcasecmp(last_key, "name") == 0) {
json_node->name = scalloc(len + 1, 1);
@ -336,13 +349,12 @@ static int json_string(void *ctx, const unsigned char *val, size_t len) {
LOG("Unhandled \"last_splitlayout\": %s\n", buf);
free(buf);
} else if (strcasecmp(last_key, "mark") == 0) {
DLOG("Found deprecated key \"mark\".\n");
char *buf = NULL;
sasprintf(&buf, "%.*s", (int)len, val);
/* We unmark any containers using this mark to avoid duplicates. */
con_unmark(buf);
json_node->mark = buf;
con_mark(json_node, buf, MM_REPLACE);
} else if (strcasecmp(last_key, "floating") == 0) {
char *buf = NULL;
sasprintf(&buf, "%.*s", (int)len, val);
@ -589,6 +601,7 @@ void tree_append_json(Con *con, const char *filename, char **errormsg) {
parsing_window_rect = false;
parsing_geometry = false;
parsing_focus = false;
parsing_marks = false;
setlocale(LC_NUMERIC, "C");
stat = yajl_parse(hand, (const unsigned char *)buf, n);
if (stat != yajl_status_ok) {

View File

@ -279,6 +279,11 @@ void match_parse_property(Match *match, const char *ctype, const char *cvalue) {
}
if (strcmp(ctype, "con_id") == 0) {
if (strcmp(cvalue, "__focused__") == 0) {
match->con_id = focused;
return;
}
char *end;
long parsed = strtol(cvalue, &end, 10);
if (parsed == LONG_MIN ||

38
src/x.c
View File

@ -545,18 +545,34 @@ void x_draw_decoration(Con *con) {
int indent_px = (indent_level * 5) * indent_mult;
int mark_width = 0;
if (config.show_marks && con->mark != NULL && (con->mark)[0] != '_') {
char *formatted_mark;
sasprintf(&formatted_mark, "[%s]", con->mark);
i3String *mark = i3string_from_utf8(formatted_mark);
if (config.show_marks && !TAILQ_EMPTY(&(con->marks_head))) {
char *formatted_mark = sstrdup("");
bool had_visible_mark = false;
mark_t *mark;
TAILQ_FOREACH(mark, &(con->marks_head), marks) {
if (mark->name[0] == '_')
continue;
had_visible_mark = true;
char *buf;
sasprintf(&buf, "%s[%s]", formatted_mark, mark->name);
free(formatted_mark);
formatted_mark = buf;
}
if (had_visible_mark) {
i3String *mark = i3string_from_utf8(formatted_mark);
mark_width = predict_text_width(mark);
draw_text(mark, parent->pixmap, parent->pm_gc, NULL,
con->deco_rect.x + con->deco_rect.width - mark_width - logical_px(2),
con->deco_rect.y + text_offset_y, mark_width);
I3STRING_FREE(mark);
}
FREE(formatted_mark);
mark_width = predict_text_width(mark);
draw_text(mark, parent->pixmap, parent->pm_gc, NULL,
con->deco_rect.x + con->deco_rect.width - mark_width - logical_px(2),
con->deco_rect.y + text_offset_y, mark_width);
I3STRING_FREE(mark);
}
i3String *title = win->title_format == NULL ? win->name : window_parse_title_format(win);

View File

@ -398,7 +398,7 @@ EOT
my @nodes = @{get_ws($tmp)->{floating_nodes}};
cmp_ok(@nodes, '==', 1, 'one floating container on this workspace');
is($nodes[0]->{nodes}[0]->{mark}, 'branded', "mark set (window_type = $atom)");
is_deeply($nodes[0]->{nodes}[0]->{marks}, [ 'branded' ], "mark set (window_type = $atom)");
exit_gracefully($pid);
@ -431,7 +431,7 @@ EOT
my @nodes = @{get_ws($tmp)->{floating_nodes}};
cmp_ok(@nodes, '==', 1, 'one floating container on this workspace');
is($nodes[0]->{nodes}[0]->{mark}, 'branded', "mark set (window_type = $atom)");
is_deeply($nodes[0]->{nodes}[0]->{marks}, [ 'branded' ], "mark set (window_type = $atom)");
exit_gracefully($pid);
@ -454,7 +454,7 @@ $window = open_window;
@nodes = @{get_ws('trigger')->{floating_nodes}};
cmp_ok(@nodes, '==', 1, 'one floating container on this workspace');
is($nodes[0]->{nodes}[0]->{mark}, 'triggered', "mark set for workspace criterion");
is_deeply($nodes[0]->{nodes}[0]->{marks}, [ 'triggered' ], "mark set for workspace criterion");
exit_gracefully($pid);

View File

@ -28,7 +28,7 @@ sub get_mark_for_window_on_workspace {
my ($ws, $con) = @_;
my $current = first { $_->{window} == $con->{id} } @{get_ws_content($ws)};
return $current->{mark};
return $current->{marks};
}
##############################################################
@ -41,7 +41,6 @@ cmd 'split h';
is_deeply(get_marks(), [], 'no marks set yet');
##############################################################
# 2: mark a con, check that it's marked, unmark it, check that
##############################################################
@ -98,7 +97,7 @@ cmd 'mark important';
cmd 'focus left';
cmd 'mark important';
is(get_mark_for_window_on_workspace($tmp, $first), 'important', 'first container now has the mark');
is_deeply(get_mark_for_window_on_workspace($tmp, $first), [ 'important' ], 'first container now has the mark');
ok(!get_mark_for_window_on_workspace($tmp, $second), 'second container lost the mark');
##############################################################
@ -116,7 +115,7 @@ ok(!get_mark_for_window_on_workspace($tmp, $con), 'container no longer has the m
$con = open_window;
cmd 'mark --toggle important';
is(get_mark_for_window_on_workspace($tmp, $con), 'important', 'container now has the mark');
is_deeply(get_mark_for_window_on_workspace($tmp, $con), [ 'important' ], 'container now has the mark');
##############################################################
# 7: mark a con, toggle a different mark, check it is marked
@ -125,8 +124,8 @@ is(get_mark_for_window_on_workspace($tmp, $con), 'important', 'container now has
$con = open_window;
cmd 'mark boring';
cmd 'mark --toggle important';
is(get_mark_for_window_on_workspace($tmp, $con), 'important', 'container has the most recent mark');
cmd 'mark --replace --toggle important';
is_deeply(get_mark_for_window_on_workspace($tmp, $con), [ 'important' ], 'container has the most recent mark');
##############################################################
# 8: mark a con, toggle the mark on another con,
@ -140,7 +139,7 @@ cmd 'mark important';
cmd 'focus left';
cmd 'mark --toggle important';
is(get_mark_for_window_on_workspace($tmp, $first), 'important', 'left container has the mark now');
is_deeply(get_mark_for_window_on_workspace($tmp, $first), [ 'important' ], 'left container has the mark now');
ok(!get_mark_for_window_on_workspace($tmp, $second), 'second containr no longer has the mark');
##############################################################

View File

@ -63,7 +63,7 @@ is($con->{window_properties}->{instance}, 'special',
# The mark `special_class_mark` is added in a `for_window` assignment in the
# config for testing purposes
is($con->{mark}, 'special_class_mark',
is_deeply($con->{marks}, [ 'special_class_mark' ],
'A `for_window` assignment should run for a match when the window changes class');
change_window_class($win, "abcdefghijklmnopqrstuv\0abcd", 24);

View File

@ -0,0 +1,117 @@
#!perl
# vim:ts=4:sw=4:expandtab
#
# Please read the following documents before working on tests:
# • http://build.i3wm.org/docs/testsuite.html
# (or docs/testsuite)
#
# • http://build.i3wm.org/docs/lib-i3test.html
# (alternatively: perldoc ./testcases/lib/i3test.pm)
#
# • http://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)
#
# Tests for mark/unmark with multiple marks on a single window.
# Ticket: #2014
use i3test;
use List::Util qw(first);
my ($ws, $con, $first, $second);
sub get_marks {
return i3(get_socket_path())->get_marks->recv;
}
sub get_mark_for_window_on_workspace {
my ($ws, $con) = @_;
my $current = first { $_->{window} == $con->{id} } @{get_ws_content($ws)};
return $current->{marks};
}
###############################################################################
# Verify that multiple marks can be set on a window.
###############################################################################
$ws = fresh_workspace;
$con = open_window;
cmd 'mark --add A';
cmd 'mark --add B';
is_deeply(sort(get_marks()), [ 'A', 'B' ], 'both marks exist');
is_deeply(get_mark_for_window_on_workspace($ws, $con), [ 'A', 'B' ], 'both marks are on the same window');
cmd 'unmark';
###############################################################################
# Verify that toggling a mark can affect only the specified mark.
###############################################################################
$ws = fresh_workspace;
$con = open_window;
cmd 'mark A';
cmd 'mark --add --toggle B';
is_deeply(get_mark_for_window_on_workspace($ws, $con), [ 'A', 'B' ], 'both marks are on the same window');
cmd 'mark --add --toggle B';
is_deeply(get_mark_for_window_on_workspace($ws, $con), [ 'A' ], 'only mark B has been removed');
cmd 'unmark';
###############################################################################
# Verify that unmarking a mark leaves other marks on the same window intact.
###############################################################################
$ws = fresh_workspace;
$con = open_window;
cmd 'mark --add A';
cmd 'mark --add B';
cmd 'mark --add C';
cmd 'unmark B';
is_deeply(get_mark_for_window_on_workspace($ws, $con), [ 'A', 'C' ], 'only mark B has been removed');
cmd 'unmark';
###############################################################################
# Verify that matching via mark works on windows with multiple marks.
###############################################################################
$ws = fresh_workspace;
$con = open_window;
cmd 'mark --add A';
cmd 'mark --add B';
open_window;
cmd '[con_mark=B] mark --add C';
is_deeply(get_mark_for_window_on_workspace($ws, $con), [ 'A', 'B', 'C' ], 'matching on a mark works with multiple marks');
cmd 'unmark';
###############################################################################
# Verify that "unmark" can be matched on the focused window.
###############################################################################
$ws = fresh_workspace;
$con = open_window;
cmd 'mark --add A';
cmd 'mark --add B';
open_window;
cmd 'mark --add C';
cmd 'mark --add D';
is_deeply(sort(get_marks()), [ 'A', 'B', 'C', 'D' ], 'all marks exist');
cmd '[con_id=__focused__] unmark';
is_deeply(sort(get_marks()), [ 'A', 'B' ], 'marks on the unfocused window still exist');
is_deeply(get_mark_for_window_on_workspace($ws, $con), [ 'A', 'B' ], 'matching on con_id=__focused__ works for unmark');
cmd 'unmark';
###############################################################################
done_testing;