diff --git a/meson.build b/meson.build index 9c5739e..9a05d44 100644 --- a/meson.build +++ b/meson.build @@ -32,7 +32,8 @@ deps = [ dependency('wayland-server'), dependency('xkbcommon'), dependency('libconfig'), - dependency('json-c') + dependency('json-c'), + dependency('uuid') ] executable( diff --git a/src/commands.c b/src/commands.c index 3cd6688..d2bbb91 100644 --- a/src/commands.c +++ b/src/commands.c @@ -24,8 +24,10 @@ #include "src/server.h" #include "src/toplevel.h" #include "src/workspace.h" +#include "wlr/util/log.h" #include #include +#include #include #include @@ -37,13 +39,19 @@ void window_command(char *tokens[], int ntokens, char *response, void window_list_command(char *tokens[], int ntokens, char *response, struct turtile_context *context); void window_switch_command(char *tokens[], int ntokens, char *response, + struct turtile_context *context); +void window_cycle_command(char *tokens[], int ntokens, char *response, + struct turtile_context *context); +void window_kill_command(char *tokens[], int ntokens, char *response, struct turtile_context *context); +void window_move_to_command(char *tokens[], int ntokens, char *response, + struct turtile_context *context); void workspace_command(char *tokens[], int ntokens, char *response, struct turtile_context *context); void workspace_list_command(char *tokens[], int ntokens, char *response, - struct turtile_context *context); + struct turtile_context *context); void workspace_switch_command(char *tokens[], int ntokens, char *response, - struct turtile_context *context); + struct turtile_context *conntext); typedef struct { char *cmd_name; char *subcmd_name; @@ -56,6 +64,9 @@ static command_t commands[] = { {"exit", NULL, exit_command}, {"window", "list", window_list_command}, {"window", "switch", window_switch_command}, + {"window", "cycle", window_cycle_command}, + {"window", "kill", window_kill_command}, + {"window", "move-to", window_move_to_command}, {"window", NULL, window_command}, {"workspace", "list", workspace_list_command}, {"workspace", "switch", workspace_switch_command}, @@ -148,9 +159,15 @@ void window_list_command(char *tokens[], int ntokens, char *response, if (toplevel->xdg_toplevel) { const char *title = toplevel->xdg_toplevel->title ? toplevel->xdg_toplevel->title : "Unnamed"; + const char *app = toplevel->xdg_toplevel->app_id ? + toplevel->xdg_toplevel->title : "null"; // Create a JSON object for each window and populate its fields struct json_object *json_window = json_object_new_object(); + json_object_object_add(json_window, "id", + json_object_new_string(toplevel->id)); + json_object_object_add(json_window, "app", + json_object_new_string(app)); json_object_object_add(json_window, "title", json_object_new_string(title)); json_object_object_add(json_window, "workspace", @@ -166,21 +183,133 @@ void window_list_command(char *tokens[], int ntokens, char *response, void window_switch_command(char *tokens[], int ntokens, char *response, struct turtile_context *context){ - // Cycle to the next toplevel - // TODO: add option to switch window by name + // Switch focus to designated toplevel struct turtile_server *server = context->server; - if (wl_list_length(&server->toplevels) < 2) { - response = strdup("{\"error\": \"Only one current window open\"}"); - return; + if(ntokens >= 1){ + char *new_toplevel_id = tokens[0]; + struct turtile_toplevel *toplevel; + + wl_list_for_each(toplevel, &server->focus_toplevels, flink) { + if(strcmp(toplevel->id, new_toplevel_id) == 0){ + focus_toplevel(toplevel, toplevel->xdg_toplevel->base->surface); + snprintf(response, MAX_MSG_SIZE, + "{\"success\": \"switching focus to: %s\"}", + toplevel->xdg_toplevel->title); + return; + } + } + snprintf(response, MAX_MSG_SIZE, + "{\"error\": \"window %s not found\"}", new_toplevel_id); + + } else{ + snprintf(response, MAX_MSG_SIZE, + "{\"error\": \"missing argument: window id\"}"); } +} + +void window_cycle_command(char *tokens[], int ntokens, char *response, + struct turtile_context *context){ + // Cycle to the next toplevel in the same workspace + struct turtile_server *server = context->server; + + struct wl_list workspace_toplevels; + get_workspace_toplevels(server->active_workspace, &workspace_toplevels); + + if (wl_list_empty(&workspace_toplevels)){ + snprintf(response, MAX_MSG_SIZE, + "{\"error\": \"Workspace is empty\"}"); + return; + } else if (wl_list_length(&workspace_toplevels) < 2) { + snprintf(response, MAX_MSG_SIZE, + "{\"error\": \"Only one current window open\"}"); + return; + } + struct turtile_toplevel *next_toplevel = - wl_container_of(server->toplevels.prev, next_toplevel, link); + wl_container_of(workspace_toplevels.next, next_toplevel, auxlink); focus_toplevel(next_toplevel, next_toplevel->xdg_toplevel->base->surface); snprintf(response, MAX_MSG_SIZE, "{\"success\": \"switching focus to: %s\"}", next_toplevel->xdg_toplevel->title); } +void window_kill_command(char *tokens[], int ntokens, char *response, + struct turtile_context *context){ + // kill designated toplevel + struct turtile_server *server = context->server; + struct turtile_toplevel *toplevel; + + if(ntokens >= 1){ + char *new_toplevel_id = tokens[0]; + + wl_list_for_each(toplevel, &server->focus_toplevels, flink) { + if(strcmp(toplevel->id, new_toplevel_id) == 0){ + kill_toplevel(toplevel); + snprintf(response, MAX_MSG_SIZE, + "{\"success\": \"kill: %s\"}", + toplevel->xdg_toplevel->title); + return; + } + } + snprintf(response, MAX_MSG_SIZE, + "{\"error\": \"window %s not found\"}", new_toplevel_id); + + } else{ + toplevel = get_first_toplevel(server); + kill_toplevel(toplevel); + snprintf(response, MAX_MSG_SIZE, + "{\"success\": \"kill: %s\"}", toplevel->xdg_toplevel->title); + } +} + +void window_move_to_command(char *tokens[], int ntokens, char *response, + struct turtile_context *context){ + struct turtile_server *server = context->server; + + if (ntokens >= 1) { + char *target_workspace_name = tokens[0]; + struct turtile_workspace *target_workspace = + get_workspace(server, target_workspace_name); + + if (!target_workspace) { + snprintf(response, MAX_MSG_SIZE, + "{\"error\": \"workspace not found\"}"); + return; + } + + struct turtile_toplevel *toplevel_to_move; + if (ntokens >= 2) { + char *toplevel_id = tokens[1]; + + toplevel_to_move = get_toplevel(server, toplevel_id); + + if (!toplevel_to_move) { + snprintf(response, MAX_MSG_SIZE, + "{\"error\": \"window %s not found\"}", toplevel_id); + return; + } + } else { + toplevel_to_move = get_first_toplevel(server); + if (!toplevel_to_move) { + snprintf(response, MAX_MSG_SIZE, + "{\"error\": \"no focused window to move\"}"); + return; + } + } + + toplevel_to_move->workspace = target_workspace; + server_redraw_windows(server); + + snprintf(response, MAX_MSG_SIZE, + "{\"success\": \"moved window %s to workspace %s\"}", + toplevel_to_move->xdg_toplevel->title, target_workspace->name); + + } else { + snprintf(response, MAX_MSG_SIZE, + "{\"error\": \"missing argument: workspace name\"}"); + } +} + void workspace_command(char *tokens[], int ntokens, char *response, struct turtile_context *context){ // TODO: use this function as a help for the other workspace subcommands diff --git a/src/toplevel.c b/src/toplevel.c index 38047e1..fe159fa 100644 --- a/src/toplevel.c +++ b/src/toplevel.c @@ -29,6 +29,7 @@ #include #include #include +#include void focus_toplevel(struct turtile_toplevel *toplevel, struct wlr_surface *surface) { /* Note: this function only deals with keyboard focus. */ @@ -75,6 +76,37 @@ void focus_toplevel(struct turtile_toplevel *toplevel, struct wlr_surface *surfa server_redraw_windows(server); } +void kill_toplevel(struct turtile_toplevel *toplevel) { + struct turtile_server *server = toplevel->server; + + struct wl_list workspace_toplevels; + get_workspace_toplevels(server->active_workspace, &workspace_toplevels); + + if (wl_list_empty(&workspace_toplevels)){ + wlr_xdg_toplevel_send_close(toplevel->xdg_toplevel); + return; + } else if (wl_list_length(&workspace_toplevels) < 2) { + wlr_xdg_toplevel_send_close(toplevel->xdg_toplevel); + return; + } + + struct turtile_toplevel *next_toplevel = + wl_container_of(workspace_toplevels.next, next_toplevel, auxlink); + focus_toplevel(next_toplevel, next_toplevel->xdg_toplevel->base->surface); + + wlr_xdg_toplevel_send_close(toplevel->xdg_toplevel); +} + +struct turtile_toplevel *get_toplevel(struct turtile_server *server, char *id) { + struct turtile_toplevel *toplevel; + wl_list_for_each(toplevel, &server->toplevels, link) { + if (strcmp(toplevel->id, id) == 0) { + return toplevel; + } + } + return NULL; +} + struct turtile_toplevel *get_first_toplevel(struct turtile_server *server) { struct turtile_toplevel *toplevel; wl_list_for_each(toplevel, &server->focus_toplevels, flink) @@ -131,6 +163,13 @@ void xdg_toplevel_map(struct wl_listener *listener, void *data) { /* Called when the surface is mapped, or ready to display on-screen. */ struct turtile_toplevel *toplevel = wl_container_of(listener, toplevel, map); + uuid_t uuid; + uuid_generate(uuid); + // Convert first 4 bytes of UUID to a short 8-character hexadecimal string + char short_uuid_str[9]; // 8 characters + null terminator + snprintf(short_uuid_str, sizeof(short_uuid_str), "%08x", *(uint32_t*)uuid); + strncpy(toplevel->id, short_uuid_str, sizeof(toplevel->id)); + toplevel->workspace = toplevel->server->active_workspace; wl_list_insert(&toplevel->server->toplevels, &toplevel->link); wl_list_insert(&toplevel->server->focus_toplevels, &toplevel->flink); diff --git a/src/toplevel.h b/src/toplevel.h index 9fdd4d9..0f81bef 100644 --- a/src/toplevel.h +++ b/src/toplevel.h @@ -27,14 +27,20 @@ #include "src/output.h" #include #include +#include struct turtile_toplevel { struct wl_list link; struct wl_list flink; + struct wl_list auxlink; + + char id[9]; // 8 characters + null terminator struct turtile_server *server; struct wlr_xdg_toplevel *xdg_toplevel; struct wlr_scene_tree *scene_tree; + struct turtile_workspace *workspace; struct wlr_box geometry; + struct wl_listener map; struct wl_listener unmap; struct wl_listener commit; @@ -43,8 +49,6 @@ struct turtile_toplevel { struct wl_listener request_resize; struct wl_listener request_maximize; struct wl_listener request_fullscreen; - - struct turtile_workspace *workspace; }; /** @@ -56,6 +60,22 @@ struct turtile_toplevel { */ void focus_toplevel(struct turtile_toplevel *toplevel, struct wlr_surface *surface); +/** + * Send close request to toplevel, and focus next window + * + * @param toplevel The turtile toplevel to kill. + */ +void kill_toplevel(struct turtile_toplevel *toplevel); + +/** + * Retrieves the toplevel with the given ID from the given server. + * + * @param server The turtile server to search for the toplevel on. + * @param id The ID of the toplevel to retrieve. + * @return A pointer to the toplevel with the given ID, or NULL + */ +struct turtile_toplevel *get_toplevel(struct turtile_server *server, char *id); + /** * Retrieves the first toplevel on the active workspace of the given server. * If no such toplevel is found, NULL is returned. diff --git a/src/workspace.c b/src/workspace.c index 0682f15..b287041 100644 --- a/src/workspace.c +++ b/src/workspace.c @@ -41,6 +41,17 @@ struct turtile_workspace* create_workspace(struct turtile_server *server, return new_workspace; } +struct turtile_workspace *get_workspace(struct turtile_server *server, + char *name) { + struct turtile_workspace *workspace; + wl_list_for_each(workspace, &server->workspaces, link) { + if (strcmp(workspace->name, name) == 0) { + return workspace; + } + } + return NULL; +} + void switch_workspace(struct turtile_workspace *workspace){ if(workspace == NULL){ return; @@ -65,3 +76,14 @@ struct turtile_workspace* create_workspaces_from_config(struct turtile_server *s } return active_workspace; } + +void get_workspace_toplevels(struct turtile_workspace *workspace, + struct wl_list *toplevels) { + struct turtile_server *server = workspace->server; + wl_list_init(toplevels); + + struct turtile_toplevel *toplevel; + wl_list_for_each(toplevel, &server->focus_toplevels, flink) + if (toplevel->workspace == workspace) + wl_list_insert(toplevels, &toplevel->auxlink); +} diff --git a/src/workspace.h b/src/workspace.h index a2145fe..c1d301d 100644 --- a/src/workspace.h +++ b/src/workspace.h @@ -43,6 +43,16 @@ struct turtile_workspace { */ struct turtile_workspace* create_workspace(struct turtile_server *server, char *name); + +/** + * Retrieves the workspace with the given name from the given server. + * + * @param server The turtile server to search for the workspace on. + * @param name The name of the workspace to retrieve. + * @return A pointer to the workspace with the given name, or NULL + */ +struct turtile_workspace *get_workspace(struct turtile_server *server, + char *name); /** * Switches the active workspace to the specified workspace. * @@ -61,4 +71,13 @@ void switch_workspace(struct turtile_workspace *workspace); */ struct turtile_workspace* create_workspaces_from_config(struct turtile_server *server); +/** + * Retrieves the list of toplevel windows in the specified workspace. + * + * @param workspace The workspace to retrieve toplevel windows from. + * @param toplevels The list to store the toplevel windows in, making use of + * auxlink +*/ +void get_workspace_toplevels(struct turtile_workspace *workspace, + struct wl_list *toplevels); #endif // WORKSPACE_H diff --git a/tests/test.py b/tests/test.py index 3ade31c..3ec230f 100644 --- a/tests/test.py +++ b/tests/test.py @@ -1,4 +1,5 @@ import subprocess +import json TTCLI = "./build/ttcli --json " @@ -7,14 +8,22 @@ def run_ttcli(command): return subprocess.run([TTCLI + command], capture_output=True, text=True, shell=True) def test_workspace_list(expected_output): - """Check list of workspaces.""" + """Check list of workspaces, ignoring irrelevant fields.""" result = run_ttcli('workspace list') - assert expected_output in result.stdout, f"Expected '{expected_output}' in output, but got:\n{result.stdout}" + workspaces = json.loads(result.stdout) -def test_window_list(expected_output): - """Check list of windows.""" + # Extract only 'name' and 'active' fields for comparison + actual_output = [{ "name": w["name"], "active": w["active"] } for w in workspaces] + assert actual_output == expected_output, f"Expected {expected_output} but got {actual_output}" + +def test_window_list(expected_titles): + """Check list of windows, ignoring IDs.""" result = run_ttcli('window list') - assert expected_output in result.stdout, f"Expected '{expected_output}' in output, but got:\n{result.stdout}" + windows = json.loads(result.stdout) + + # Extract 'title' and 'workspace' fields only + actual_titles = [{ "title": w["title"], "workspace": w["workspace"] } for w in windows] + assert actual_titles == expected_titles, f"Expected {expected_titles} but got {actual_titles}" def test_workspace_switch(destination_workspace): """Check workspace switch.""" @@ -23,8 +32,17 @@ def test_workspace_switch(destination_workspace): assert expected_success_message in result.stdout, f"Expected 'switch to workspace {destination_workspace}' in output, but got:\n{result.stdout}" if __name__ == '__main__': - test_workspace_list('[ { "name": "main", "active": true }, { "name": "test", "active": false } ]') - test_window_list('[ { "title": "simple-egl", "workspace": "main" }, { "title": "simple-damage", "workspace": "main" } ]') + test_workspace_list([ + { "name": "main", "active": True }, + { "name": "test", "active": False } + ]) + test_window_list([ + { "title": "simple-egl", "workspace": "main" }, + { "title": "simple-damage", "workspace": "main" } + ]) test_workspace_switch('test') - test_workspace_list('[ { "name": "main", "active": false }, { "name": "test", "active": true } ]') + test_workspace_list([ + { "name": "main", "active": False }, + { "name": "test", "active": True } + ]) run_ttcli('exit')