From b48239588f103a45b94c246be2479e96838e64df Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sat, 27 Dec 2025 09:35:00 +0000 Subject: [PATCH] feat(workspace): add marks support to workspaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- include/sway/tree/workspace.h | 11 +++++++ sway/commands/mark.c | 57 ++++++++++++++++++++++++++--------- sway/commands/move.c | 14 ++++++--- sway/commands/unmark.c | 45 +++++++++++++++++++-------- sway/criteria.c | 32 +++++++++++++++++--- sway/ipc-json.c | 8 +++++ sway/tree/workspace.c | 53 ++++++++++++++++++++++++++++++++ 7 files changed, 183 insertions(+), 37 deletions(-) diff --git a/include/sway/tree/workspace.h b/include/sway/tree/workspace.h index 27ed649fd..538cf85d4 100644 --- a/include/sway/tree/workspace.h +++ b/include/sway/tree/workspace.h @@ -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 diff --git a/sway/commands/mark.c b/sway/commands/mark.c index 77c8d2394..4bb960fcb 100644 --- a/sway/commands/mark.c +++ b/sway/commands/mark.c @@ -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); diff --git a/sway/commands/move.c b/sway/commands/move.c index 90e8585b4..7705200c4 100644 --- a/sway/commands/move.c +++ b/sway/commands/move.c @@ -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); } diff --git a/sway/commands/unmark.c b/sway/commands/unmark.c index 4aba5bae0..e4b585a0a 100644 --- a/sway/commands/unmark.c +++ b/sway/commands/unmark.c @@ -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); diff --git a/sway/criteria.c b/sway/criteria.c index 8c6bd7249..e9eafc12b 100644 --- a/sway/criteria.c +++ b/sway/criteria.c @@ -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; diff --git a/sway/ipc-json.c b/sway/ipc-json.c index 3b69ad384..ae76f8ce1 100644 --- a/sway/ipc-json.c +++ b/sway/ipc-json.c @@ -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) { diff --git a/sway/tree/workspace.c b/sway/tree/workspace.c index 733a002b4..e24fa0e7b 100644 --- a/sway/tree/workspace.c +++ b/sway/tree/workspace.c @@ -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; +}