feat(workspace): add marks support to workspaces

Workspaces can now have marks just like containers. This enables the
pattern of marking a workspace with [con_id=X] and then using
"move container to mark" to reparent containers to workspaces.

Changes:
- Add marks list to sway_workspace struct
- Add workspace_has_mark, workspace_add_mark, workspace_clear_marks,
  workspace_find_and_unmark, workspace_find_mark functions
- Update mark command to work on workspaces when targeted via criteria
- Update unmark command to support workspaces
- Update move.c to find marks on workspaces for "move to mark"
- Update criteria con_mark matching to also check workspaces
- Add workspace marks to IPC JSON output

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude Code 2025-12-27 09:35:00 +00:00 committed by Vladimir Panteleev
parent e7bd3f14e5
commit b48239588f
No known key found for this signature in database
GPG key ID: 5004F0FAD051576D
7 changed files with 183 additions and 37 deletions

View file

@ -48,6 +48,7 @@ struct sway_workspace {
list_t *floating; // struct sway_container
list_t *tiling; // struct sway_container
list_t *output_priority;
list_t *marks; // char *
bool urgent;
struct sway_workspace_state current;
@ -157,4 +158,14 @@ size_t workspace_num_sticky_containers(struct sway_workspace *ws);
*/
void workspace_squash(struct sway_workspace *workspace);
bool workspace_has_mark(struct sway_workspace *ws, char *mark);
void workspace_add_mark(struct sway_workspace *ws, char *mark);
void workspace_clear_marks(struct sway_workspace *ws);
bool workspace_find_and_unmark(char *mark);
struct sway_workspace *workspace_find_mark(char *mark);
#endif

View file

@ -2,6 +2,7 @@
#include "sway/commands.h"
#include "sway/config.h"
#include "sway/tree/view.h"
#include "sway/tree/workspace.h"
#include "list.h"
#include "log.h"
#include "stringop.h"
@ -18,8 +19,13 @@ struct cmd_results *cmd_mark(int argc, char **argv) {
return error;
}
struct sway_container *container = config->handler_context.container;
if (!container) {
return cmd_results_new(CMD_INVALID, "Only containers can have marks");
struct sway_workspace *workspace = config->handler_context.workspace;
// If no container but we have a workspace (targeted via con_id criteria),
// apply marks to the workspace
if (!container && !workspace) {
return cmd_results_new(CMD_INVALID,
"Only containers and workspaces can have marks");
}
bool add = false, toggle = false;
@ -44,23 +50,44 @@ struct cmd_results *cmd_mark(int argc, char **argv) {
}
char *mark = join_args(argv, argc);
bool had_mark = container_has_mark(container, mark);
if (!add) {
// Replacing
container_clear_marks(container);
}
if (container) {
bool had_mark = container_has_mark(container, mark);
container_find_and_unmark(mark);
if (!add) {
// Replacing
container_clear_marks(container);
}
if (!toggle || !had_mark) {
container_add_mark(container, mark);
}
container_find_and_unmark(mark);
workspace_find_and_unmark(mark);
free(mark);
container_update_marks(container);
if (container->view) {
view_execute_criteria(container->view);
if (!toggle || !had_mark) {
container_add_mark(container, mark);
}
free(mark);
container_update_marks(container);
if (container->view) {
view_execute_criteria(container->view);
}
} else {
// Workspace
bool had_mark = workspace_has_mark(workspace, mark);
if (!add) {
// Replacing
workspace_clear_marks(workspace);
}
container_find_and_unmark(mark);
workspace_find_and_unmark(mark);
if (!toggle || !had_mark) {
workspace_add_mark(workspace, mark);
}
free(mark);
}
return cmd_results_new(CMD_SUCCESS, NULL);

View file

@ -525,11 +525,17 @@ static struct cmd_results *cmd_move_container(bool no_auto_back_and_forth,
destination = seat_get_focus_inactive(seat, &new_output->node);
} else if (strcasecmp(argv[0], "mark") == 0) {
struct sway_container *dest_con = container_find_mark(argv[1]);
if (dest_con == NULL) {
return cmd_results_new(CMD_FAILURE,
"Mark '%s' not found", argv[1]);
if (dest_con) {
destination = &dest_con->node;
} else {
struct sway_workspace *dest_ws = workspace_find_mark(argv[1]);
if (dest_ws) {
destination = &dest_ws->node;
} else {
return cmd_results_new(CMD_FAILURE,
"Mark '%s' not found", argv[1]);
}
}
destination = &dest_con->node;
} else {
return cmd_results_new(CMD_INVALID, "%s", expected_syntax);
}

View file

@ -3,29 +3,38 @@
#include "sway/config.h"
#include "sway/tree/root.h"
#include "sway/tree/view.h"
#include "sway/tree/workspace.h"
#include "list.h"
#include "log.h"
#include "stringop.h"
static void remove_mark(struct sway_container *con) {
static void remove_container_mark(struct sway_container *con) {
container_clear_marks(con);
container_update_marks(con);
}
static void remove_all_marks_iterator(struct sway_container *con, void *data) {
remove_mark(con);
static void remove_all_container_marks_iterator(struct sway_container *con, void *data) {
remove_container_mark(con);
}
// unmark Remove all marks from all views
// unmark foo Remove single mark from whichever view has it
// [criteria] unmark Remove all marks from matched view
// [criteria] unmark foo Remove single mark from matched view
static void remove_all_workspace_marks_iterator(struct sway_workspace *ws, void *data) {
workspace_clear_marks(ws);
}
// unmark Remove all marks from all views/workspaces
// unmark foo Remove single mark from whichever view/workspace has it
// [criteria] unmark Remove all marks from matched view/workspace
// [criteria] unmark foo Remove single mark from matched view/workspace
struct cmd_results *cmd_unmark(int argc, char **argv) {
// Determine the container
// Determine the container or workspace
struct sway_container *con = NULL;
struct sway_workspace *ws = NULL;
if (config->handler_context.node_overridden) {
con = config->handler_context.container;
if (!con) {
ws = config->handler_context.workspace;
}
}
// Determine the mark
@ -41,13 +50,23 @@ struct cmd_results *cmd_unmark(int argc, char **argv) {
}
} else if (con && !mark) {
// Clear all marks from the given container
remove_mark(con);
} else if (!con && mark) {
// Remove mark from whichever container has it
remove_container_mark(con);
} else if (ws && mark) {
// Remove the mark from the given workspace
if (workspace_has_mark(ws, mark)) {
workspace_find_and_unmark(mark);
}
} else if (ws && !mark) {
// Clear all marks from the given workspace
workspace_clear_marks(ws);
} else if (!con && !ws && mark) {
// Remove mark from whichever container/workspace has it
container_find_and_unmark(mark);
workspace_find_and_unmark(mark);
} else {
// Remove all marks from all containers
root_for_each_container(remove_all_marks_iterator, NULL);
// Remove all marks from all containers and workspaces
root_for_each_container(remove_all_container_marks_iterator, NULL);
root_for_each_workspace(remove_all_workspace_marks_iterator, NULL);
}
free(mark);

View file

@ -507,12 +507,34 @@ static void criteria_get_nodes_container_iterator(struct sway_container *contain
}
}
static bool criteria_matches_workspace(struct criteria *criteria,
struct sway_workspace *workspace) {
if (criteria->con_mark) {
bool exists = false;
for (int i = 0; i < workspace->marks->length; ++i) {
if (regex_cmp(workspace->marks->items[i], criteria->con_mark->regex) >= 0) {
exists = true;
break;
}
}
if (!exists) {
return false;
}
}
if (criteria->con_id) {
if (workspace->node.id != criteria->con_id) {
return false;
}
}
return true;
}
static void criteria_get_nodes_workspace_iterator(struct sway_workspace *workspace,
void *data) {
struct match_data *match_data = data;
// Workspaces only support con_id matching (not con_mark, since they don't have marks)
if (match_data->criteria->con_id &&
workspace->node.id == match_data->criteria->con_id) {
if (criteria_matches_workspace(match_data->criteria, workspace)) {
list_add(match_data->matches, &workspace->node);
}
}
@ -524,8 +546,8 @@ list_t *criteria_get_nodes(struct criteria *criteria) {
.matches = matches,
};
root_for_each_container(criteria_get_nodes_container_iterator, &data);
// Also check workspaces for con_id matching
if (criteria->con_id && !criteria->con_mark) {
// Also check workspaces for con_id and con_mark matching
if (criteria->con_id || criteria->con_mark) {
root_for_each_workspace(criteria_get_nodes_workspace_iterator, &data);
}
return matches;

View file

@ -530,6 +530,14 @@ static void ipc_json_describe_workspace(struct sway_workspace *workspace,
json_object_new_string(
ipc_json_orientation_description(workspace->layout)));
// Marks
json_object *marks = json_object_new_array();
for (int i = 0; i < workspace->marks->length; ++i) {
json_object_array_add(marks,
json_object_new_string(workspace->marks->items[i]));
}
json_object_object_add(object, "marks", marks);
// Floating
json_object *floating_array = json_object_new_array();
for (int i = 0; i < workspace->floating->length; ++i) {

View file

@ -89,6 +89,7 @@ struct sway_workspace *workspace_create(struct sway_output *output,
ws->floating = create_list();
ws->tiling = create_list();
ws->output_priority = create_list();
ws->marks = create_list();
ws->gaps_outer = config->gaps_outer;
ws->gaps_inner = config->gaps_inner;
@ -151,6 +152,7 @@ void workspace_destroy(struct sway_workspace *workspace) {
free(workspace->name);
free(workspace->representation);
list_free_items_and_destroy(workspace->output_priority);
list_free_items_and_destroy(workspace->marks);
list_free(workspace->floating);
list_free(workspace->tiling);
list_free(workspace->current.floating);
@ -979,3 +981,54 @@ void workspace_squash(struct sway_workspace *workspace) {
i += container_squash(child);
}
}
bool workspace_has_mark(struct sway_workspace *ws, char *mark) {
for (int i = 0; i < ws->marks->length; ++i) {
char *item = ws->marks->items[i];
if (strcmp(item, mark) == 0) {
return true;
}
}
return false;
}
void workspace_add_mark(struct sway_workspace *ws, char *mark) {
list_add(ws->marks, strdup(mark));
ipc_event_workspace(NULL, ws, "mark");
}
void workspace_clear_marks(struct sway_workspace *ws) {
for (int i = 0; i < ws->marks->length; ++i) {
free(ws->marks->items[i]);
}
ws->marks->length = 0;
ipc_event_workspace(NULL, ws, "mark");
}
static bool find_by_mark_iterator_ws(struct sway_workspace *ws, void *data) {
char *mark = data;
return workspace_has_mark(ws, mark);
}
struct sway_workspace *workspace_find_mark(char *mark) {
return root_find_workspace(find_by_mark_iterator_ws, mark);
}
bool workspace_find_and_unmark(char *mark) {
struct sway_workspace *ws = root_find_workspace(
find_by_mark_iterator_ws, mark);
if (!ws) {
return false;
}
for (int i = 0; i < ws->marks->length; ++i) {
char *ws_mark = ws->marks->items[i];
if (strcmp(ws_mark, mark) == 0) {
free(ws_mark);
list_del(ws->marks, i);
ipc_event_workspace(NULL, ws, "mark");
return true;
}
}
return false;
}