From 4d21f4cfc2cfd3166164218f665f89b9ec8a69a5 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Tue, 11 Sep 2018 19:11:05 +0300 Subject: [PATCH 01/98] init_ws_for_output: use workspace_move_to_output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes a crash produced with the following config: # i3 config file (v4) workspace 1 output $screen1 workspace 2 output $screen2 exec --no-startup-id "i3-msg workspace 1, open && i3-msg workspace 2 && xrandr --output $screen2 --off && xrandr --output $screen1 --auto --output $screen2 --auto --right-of $screen1 " Which results in: ERROR: AddressSanitizer: heap-use-after-free on address … READ of size 8 at 0x614000001f48 thread T0 #0 0x5563df6e73a8 in init_ws_for_output i3/src/randr.c:468 #1 0x5563df6ef3b4 in randr_query_outputs i3/src/randr.c:940 #2 0x5563df68dbe1 in handle_screen_change i3/src/handlers.c:450 … is located 264 bytes inside of 448-byte region … freed by thread T0 here: #1 0x5563df634b0a in con_free i3/src/con.c:96 #2 0x5563df7151e6 in tree_close_internal i3/src/tree.c:344 #3 0x5563df7280fe in workspace_show i3/src/workspace.c:499 #4 0x5563df6e7315 in init_ws_for_output i3/src/randr.c:457 #5 0x5563df6ef3b4 in randr_query_outputs i3/src/randr.c:940 #6 0x5563df68dbe1 in handle_screen_change i3/src/handlers.c:450 Which is similar to #3228, #3248. --- src/randr.c | 48 +++++++----------------------------------------- src/workspace.c | 10 +++++++++- 2 files changed, 16 insertions(+), 42 deletions(-) diff --git a/src/randr.c b/src/randr.c index b1a9e8cb..0c1f9704 100644 --- a/src/randr.c +++ b/src/randr.c @@ -442,47 +442,13 @@ void init_ws_for_output(Output *output, Con *content) { continue; } - /* if so, move it over */ - LOG("Moving workspace \"%s\" from output \"%s\" to \"%s\" due to assignment\n", - workspace->name, workspace_out->name, output_primary_name(output)); - - /* if the workspace is currently visible on that output, we need to - * switch to a different workspace - otherwise the output would end up - * with no active workspace */ - bool visible = workspace_is_visible(workspace); - Con *previous = NULL; - if (visible && (previous = TAILQ_NEXT(workspace, focused))) { - LOG("Switching to previously used workspace \"%s\" on output \"%s\"\n", - previous->name, workspace_out->name); - workspace_show(previous); - } - - /* Render the output on which the workspace was to get correct Rects. - * Then, we need to work with the "content" container, since we cannot - * be sure that the workspace itself was rendered at all (in case it’s - * invisible, it won’t be rendered). */ - render_con(workspace_out, false); - Con *ws_out_content = output_get_content(workspace_out); - - Con *floating_con; - TAILQ_FOREACH(floating_con, &(workspace->floating_head), floating_windows) - /* NB: We use output->con here because content is not yet rendered, - * so it has a rect of {0, 0, 0, 0}. */ - floating_fix_coordinates(floating_con, &(ws_out_content->rect), &(output->con->rect)); - - con_detach(workspace); - con_attach(workspace, content, false); - - /* In case the workspace we just moved was visible but there was no - * other workspace to switch to, we need to initialize the source - * output as well */ - if (visible && previous == NULL) { - LOG("There is no workspace left on \"%s\", re-initializing\n", - workspace_out->name); - init_ws_for_output(get_output_by_name(workspace_out->name, true), - output_get_content(workspace_out)); - DLOG("Done re-initializing, continuing with \"%s\"\n", output_primary_name(output)); - } + DLOG("Moving workspace \"%s\" from output \"%s\" to \"%s\" due to assignment\n", + workspace->name, workspace_out->name, output_primary_name(output)); + /* Need to copy output's rect since content is not yet rendered. We + * can't call render_con here because render_output only proceeds if a + * workspace exists. */ + content->rect = output->con->rect; + workspace_move_to_output(workspace, output); } /* if a workspace exists, we are done now */ diff --git a/src/workspace.c b/src/workspace.c index e92aaa5f..2a9f88db 100644 --- a/src/workspace.c +++ b/src/workspace.c @@ -966,7 +966,11 @@ bool workspace_move_to_output(Con *ws, Output *output) { LOG("got output %p with content %p\n", output, content); Con *previously_visible_ws = TAILQ_FIRST(&(content->focus_head)); - LOG("Previously visible workspace = %p / %s\n", previously_visible_ws, previously_visible_ws->name); + if (previously_visible_ws) { + DLOG("Previously visible workspace = %p / %s\n", previously_visible_ws, previously_visible_ws->name); + } else { + DLOG("No previously visible workspace on output.\n"); + } bool workspace_was_visible = workspace_is_visible(ws); if (con_num_children(ws->parent) == 1) { @@ -1024,6 +1028,10 @@ bool workspace_move_to_output(Con *ws, Output *output) { workspace_show(ws); } + if (!previously_visible_ws) { + return true; + } + /* NB: We cannot simply work with previously_visible_ws since it might * have been cleaned up by workspace_show() already, depending on the * focus order/number of other workspaces on the output. From db3b9e41874400958cf85b461a5d1ff04a398fa3 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Tue, 11 Sep 2018 20:09:16 +0300 Subject: [PATCH 02/98] randr_disable_output: Always restore focus con_detach and con_attach modify the focus stack. This will make sure that the currently focused workspace will remain focused after disabling an output. --- src/randr.c | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/randr.c b/src/randr.c index 0c1f9704..4123cfcf 100644 --- a/src/randr.c +++ b/src/randr.c @@ -942,13 +942,8 @@ void randr_disable_output(Output *output) { if (output->con != NULL) { /* We need to move the workspaces from the disappearing output to the first output */ - /* 1: Get the con to focus next, if the disappearing ws is focused */ - Con *next = NULL; - if (TAILQ_FIRST(&(croot->focus_head)) == output->con) { - DLOG("This output (%p) was focused! Getting next\n", output->con); - next = focused; - DLOG("next = %p\n", next); - } + /* 1: Get the con to focus next */ + Con *next = focused; /* 2: iterate through workspaces and re-assign them, fixing the coordinates * of floating containers as we go */ @@ -971,15 +966,12 @@ void randr_disable_output(Output *output) { TAILQ_FOREACH(floating_con, &(current->floating_head), floating_windows) { floating_fix_coordinates(floating_con, &(output->con->rect), &(first->con->rect)); } - DLOG("Done, next\n"); } - DLOG("re-attached all workspaces\n"); - if (next) { - DLOG("now focusing next = %p\n", next); - con_activate(next); - workspace_show(con_get_workspace(next)); - } + /* Restore focus after con_detach / con_attach */ + DLOG("now focusing next = %p\n", next); + con_focus(next); + workspace_show(con_get_workspace(next)); /* 3: move the dock clients to the first output */ Con *child; From 5976381012ab1ce382ef6eb8e432ec39362a03c5 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Tue, 11 Sep 2018 20:39:33 +0300 Subject: [PATCH 03/98] output_init_con: Restore focus if possible Before this, i3 would focus newly created workspaces on output init --- src/randr.c | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/randr.c b/src/randr.c index 4123cfcf..7c90f1c0 100644 --- a/src/randr.c +++ b/src/randr.c @@ -421,6 +421,8 @@ void output_init_con(Output *output) { * */ void init_ws_for_output(Output *output, Con *content) { + Con *previous_focus = con_get_workspace(focused); + /* go through all assignments and move the existing workspaces to this output */ struct Workspace_Assignment *assignment; TAILQ_FOREACH(assignment, &ws_assignments, ws_assignments) { @@ -451,6 +453,9 @@ void init_ws_for_output(Output *output, Con *content) { workspace_move_to_output(workspace, output); } + /* Temporarily set the focused container, might not be initialized yet. */ + focused = content; + /* if a workspace exists, we are done now */ if (!TAILQ_EMPTY(&(content->nodes_head))) { /* ensure that one of the workspaces is actually visible (in fullscreen @@ -459,10 +464,9 @@ void init_ws_for_output(Output *output, Con *content) { GREP_FIRST(visible, content, child->fullscreen_mode == CF_OUTPUT); if (!visible) { visible = TAILQ_FIRST(&(content->nodes_head)); - focused = content; workspace_show(visible); } - return; + goto restore_focus; } /* otherwise, we create the first assigned ws for this output */ @@ -473,17 +477,18 @@ void init_ws_for_output(Output *output, Con *content) { LOG("Initializing first assigned workspace \"%s\" for output \"%s\"\n", assignment->name, assignment->output); - focused = content; workspace_show_by_name(assignment->name); - return; + goto restore_focus; } /* if there is still no workspace, we create the first free workspace */ DLOG("Now adding a workspace\n"); - Con *ws = create_workspace_on_output(output, content); + workspace_show(create_workspace_on_output(output, content)); - /* TODO: Set focus in main.c */ - con_focus(ws); +restore_focus: + if (previous_focus) { + workspace_show(previous_focus); + } } /* From f6bb1e22bb62d483f883e53c8749d4ec288ab7c0 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Wed, 12 Sep 2018 16:53:20 +0300 Subject: [PATCH 04/98] init_ws_for_output: Remove content argument --- include/randr.h | 2 +- src/fake_outputs.c | 2 +- src/randr.c | 7 ++++--- src/xinerama.c | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/include/randr.h b/include/randr.h index 39182c54..5c0c8b3d 100644 --- a/include/randr.h +++ b/include/randr.h @@ -48,7 +48,7 @@ void output_init_con(Output *output); * • Create the first unused workspace. * */ -void init_ws_for_output(Output *output, Con *content); +void init_ws_for_output(Output *output); /** * Initializes the specified output, assigning the specified workspace to it. diff --git a/src/fake_outputs.c b/src/fake_outputs.c index 39cbd7bb..5f3622d4 100644 --- a/src/fake_outputs.c +++ b/src/fake_outputs.c @@ -74,7 +74,7 @@ void fake_outputs_init(const char *output_spec) { else TAILQ_INSERT_TAIL(&outputs, new_output, outputs); output_init_con(new_output); - init_ws_for_output(new_output, output_get_content(new_output->con)); + init_ws_for_output(new_output); num_screens++; } new_output->primary = primary; diff --git a/src/randr.c b/src/randr.c index 7c90f1c0..d22726d2 100644 --- a/src/randr.c +++ b/src/randr.c @@ -420,7 +420,8 @@ void output_init_con(Output *output) { * • Create the first unused workspace. * */ -void init_ws_for_output(Output *output, Con *content) { +void init_ws_for_output(Output *output) { + Con *content = output_get_content(output->con); Con *previous_focus = con_get_workspace(focused); /* go through all assignments and move the existing workspaces to this output */ @@ -908,7 +909,7 @@ void randr_query_outputs(void) { if (!TAILQ_EMPTY(&(content->nodes_head))) continue; DLOG("Should add ws for output %s\n", output_primary_name(output)); - init_ws_for_output(output, content); + init_ws_for_output(output); } /* Focus the primary screen, if possible */ @@ -1013,7 +1014,7 @@ void randr_disable_output(Output *output) { static void fallback_to_root_output(void) { root_output->active = true; output_init_con(root_output); - init_ws_for_output(root_output, output_get_content(root_output->con)); + init_ws_for_output(root_output); } /* diff --git a/src/xinerama.c b/src/xinerama.c index d0651a85..4acfd3cb 100644 --- a/src/xinerama.c +++ b/src/xinerama.c @@ -71,7 +71,7 @@ static void query_screens(xcb_connection_t *conn) { else TAILQ_INSERT_TAIL(&outputs, s, outputs); output_init_con(s); - init_ws_for_output(s, output_get_content(s->con)); + init_ws_for_output(s); num_screens++; } @@ -98,7 +98,7 @@ static void use_root_output(xcb_connection_t *conn) { s->active = true; TAILQ_INSERT_TAIL(&outputs, s, outputs); output_init_con(s); - init_ws_for_output(s, output_get_content(s->con)); + init_ws_for_output(s); } /* From 8e1687a317bdde3217dba895bc8f05975690896c Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Wed, 6 Sep 2017 05:44:09 +0300 Subject: [PATCH 05/98] Rewrite con_swap to work only with queue operations Benefits are that we don't open a fake container and don't call many complicated functions that can lead to redraws (x_push_changes calls) as discussed in #2954. Fixes #2810: Windows exchange floating mode & window rects. Swap will still not work with CT_FLOATING_CONs but this doesn't make much sense. Fixes #3280: The behaviour is not very user friendly but swap behaves exactly as it should. The rest is a tree_flatten issue. Attached pictures in #2954. --- docs/userguide | 3 +- src/con.c | 194 +++++++++++------------------- testcases/t/291-swap.t | 219 +++++++++++++++++++++++++++++++++- testcases/t/294-focus-order.t | 39 ++++++ 4 files changed, 324 insertions(+), 131 deletions(-) diff --git a/docs/userguide b/docs/userguide index da5d9873..f92b4bea 100644 --- a/docs/userguide +++ b/docs/userguide @@ -2092,8 +2092,7 @@ using one of the following methods: +mark+:: A container with the specified mark, see <>. Note that swapping does not work with all containers. Most notably, swapping -floating containers or containers that have a parent-child relationship to one -another does not work. +containers that have a parent-child relationship to one another does not work. *Syntax*: ---------------------------------------- diff --git a/src/con.c b/src/con.c index d50c29be..436ce776 100644 --- a/src/con.c +++ b/src/con.c @@ -2335,11 +2335,6 @@ bool con_swap(Con *first, Con *second) { return false; } - if (con_is_floating(first) || con_is_floating(second)) { - ELOG("Floating windows cannot be swapped.\n"); - return false; - } - if (first == second) { DLOG("Swapping container %p with itself, nothing to do.\n", first); return false; @@ -2350,132 +2345,80 @@ bool con_swap(Con *first, Con *second) { return false; } - Con *old_focus = focused; - - Con *first_ws = con_get_workspace(first); - Con *second_ws = con_get_workspace(second); - Con *current_ws = con_get_workspace(old_focus); - const bool focused_within_first = (first == old_focus || con_has_parent(old_focus, first)); - const bool focused_within_second = (second == old_focus || con_has_parent(old_focus, second)); - fullscreen_mode_t first_fullscreen_mode = first->fullscreen_mode; - fullscreen_mode_t second_fullscreen_mode = second->fullscreen_mode; - - if (first_fullscreen_mode != CF_NONE) { - con_disable_fullscreen(first); - } - if (second_fullscreen_mode != CF_NONE) { - con_disable_fullscreen(second); + Con *ws1 = con_get_workspace(first); + Con *ws2 = con_get_workspace(second); + Con *restore_focus = NULL; + if (ws1 == ws2 && ws1 == con_get_workspace(focused)) { + /* Preserve focus in the current workspace. */ + restore_focus = focused; + } else if (first == focused || con_has_parent(focused, first)) { + restore_focus = second; + } else if (second == focused || con_has_parent(focused, second)) { + restore_focus = first; } - double first_percent = first->percent; - double second_percent = second->percent; +#define SWAP_CONS_IN_TREE(headname, field) \ + do { \ + struct headname *head1 = &(first->parent->headname); \ + struct headname *head2 = &(second->parent->headname); \ + Con *first_prev = TAILQ_PREV(first, headname, field); \ + Con *second_prev = TAILQ_PREV(second, headname, field); \ + if (second_prev == first) { \ + TAILQ_SWAP(first, second, head1, field); \ + } else if (first_prev == second) { \ + TAILQ_SWAP(second, first, head1, field); \ + } else { \ + TAILQ_REMOVE(head1, first, field); \ + TAILQ_REMOVE(head2, second, field); \ + if (second_prev == NULL) { \ + TAILQ_INSERT_HEAD(head2, first, field); \ + } else { \ + TAILQ_INSERT_AFTER(head2, second_prev, first, field); \ + } \ + if (first_prev == NULL) { \ + TAILQ_INSERT_HEAD(head1, second, field); \ + } else { \ + TAILQ_INSERT_AFTER(head1, first_prev, second, field); \ + } \ + } \ + } while (0) - /* De- and reattaching the containers will insert them at the tail of the - * focus_heads. We will need to fix this. But we need to make sure first - * and second don't get in each other's way if they share the same parent, - * so we select the closest previous focus_head that isn't involved. */ - Con *first_prev_focus_head = first; - while (first_prev_focus_head == first || first_prev_focus_head == second) { - first_prev_focus_head = TAILQ_PREV(first_prev_focus_head, focus_head, focused); - } + SWAP_CONS_IN_TREE(nodes_head, nodes); + SWAP_CONS_IN_TREE(focus_head, focused); + SWAP(first->parent, second->parent, Con *); - Con *second_prev_focus_head = second; - while (second_prev_focus_head == second || second_prev_focus_head == first) { - second_prev_focus_head = TAILQ_PREV(second_prev_focus_head, focus_head, focused); - } - - /* We use a fake container to mark the spot of where the second container needs to go. */ - Con *fake = con_new(NULL, NULL); - fake->layout = L_SPLITH; - _con_attach(fake, first->parent, first, true); - - bool result = true; - /* Swap the containers. We set the ignore_focus flag here because after the - * container is attached, the focus order is not yet correct and would - * result in wrong windows being focused. */ - - /* Move first to second. */ - result &= _con_move_to_con(first, second, false, false, false, true, false); - /* If swapping the containers didn't work we don't need to mess with the focus. */ - if (!result) { - goto swap_end; - } - - /* If we moved the container holding the focused window to another - * workspace we need to ensure the visible workspace has the focused - * container. - * We don't need to check this for the second container because we've only - * moved the first one at this point.*/ - if (first_ws != second_ws && focused_within_first) { - con_activate(con_descend_focused(current_ws)); - } - - /* Move second to where first has been originally. */ - result &= _con_move_to_con(second, fake, false, false, false, true, false); - if (!result) { - goto swap_end; - } - - /* Swapping will have inserted the containers at the tail of their parents' - * focus head. We fix this now by putting them in the position of the focus - * head the container they swapped with was in. */ - TAILQ_REMOVE(&(first->parent->focus_head), first, focused); - TAILQ_REMOVE(&(second->parent->focus_head), second, focused); - - if (second_prev_focus_head == NULL) { - TAILQ_INSERT_HEAD(&(first->parent->focus_head), first, focused); - } else { - TAILQ_INSERT_AFTER(&(first->parent->focus_head), second_prev_focus_head, first, focused); - } - - if (first_prev_focus_head == NULL) { - TAILQ_INSERT_HEAD(&(second->parent->focus_head), second, focused); - } else { - TAILQ_INSERT_AFTER(&(second->parent->focus_head), first_prev_focus_head, second, focused); - } - - /* If the focus was within any of the swapped containers, do the following: - * - If swapping took place within a workspace, ensure the previously - * focused container stays focused. - * - Otherwise, focus the container that has been swapped in. - * - * To understand why fixing the focus_head previously wasn't enough, - * consider the scenario - * H[ V[ A X ] V[ Y B ] ] - * with B being focused, but X being the focus_head within its parent. If - * we swap A and B now, fixing the focus_head would focus X, but since B - * was the focused container before it should stay focused. - */ - if (focused_within_first) { - if (first_ws == second_ws) { - con_activate(old_focus); - } else { - con_activate(con_descend_focused(second)); - } - } else if (focused_within_second) { - if (first_ws == second_ws) { - con_activate(old_focus); - } else { - con_activate(con_descend_focused(first)); - } - } + /* Floating nodes are children of CT_FLOATING_CONs, they are listed in + * nodes_head and focus_head like all other containers. Thus, we don't need + * to do anything special other than swapping the floating status and the + * relevant rects. */ + SWAP(first->floating, second->floating, int); + SWAP(first->rect, second->rect, Rect); + SWAP(first->window_rect, second->window_rect, Rect); /* We need to copy each other's percentages to ensure that the geometry - * doesn't change during the swap. This needs to happen _before_ we close - * the fake container as closing the tree will recalculate percentages. */ - first->percent = second_percent; - second->percent = first_percent; - fake->percent = 0.0; + * doesn't change during the swap. */ + SWAP(first->percent, second->percent, double); - SWAP(first_fullscreen_mode, second_fullscreen_mode, fullscreen_mode_t); - -swap_end: - /* The two windows exchange their original fullscreen status */ - if (first_fullscreen_mode != CF_NONE) { - con_enable_fullscreen(first, first_fullscreen_mode); + if (restore_focus) { + con_focus(restore_focus); } - if (second_fullscreen_mode != CF_NONE) { - con_enable_fullscreen(second, second_fullscreen_mode); + + /* Update new parents' & workspaces' urgency. */ + con_set_urgency(first, first->urgent); + con_set_urgency(second, second->urgent); + + /* Exchange fullscreen modes, can't use SWAP because we need to call the + * correct functions. */ + fullscreen_mode_t second_fullscreen_mode = second->fullscreen_mode; + if (first->fullscreen_mode == CF_NONE) { + con_disable_fullscreen(second); + } else { + con_enable_fullscreen(second, first->fullscreen_mode); + } + if (second_fullscreen_mode == CF_NONE) { + con_disable_fullscreen(first); + } else { + con_enable_fullscreen(first, second_fullscreen_mode); } /* We don't actually need this since percentages-wise we haven't changed @@ -2484,11 +2427,8 @@ swap_end: con_fix_percent(first->parent); con_fix_percent(second->parent); - /* We can get rid of the fake container again now. */ - con_close(fake, DONT_KILL_WINDOW); - con_force_split_parents_redraw(first); con_force_split_parents_redraw(second); - return result; + return true; } diff --git a/testcases/t/291-swap.t b/testcases/t/291-swap.t index da2d564d..8705c1ec 100644 --- a/testcases/t/291-swap.t +++ b/testcases/t/291-swap.t @@ -16,20 +16,42 @@ # # Tests the swap command. # Ticket: #917 -use i3test i3_config => < 0; + +my $config = <{fullscreen_mode} != 0 } @{get_ws_content($ws)} +} + +sub cmp_floating_rect { + my ($window, $rect, $prefix) = @_; + my ($absolute, $top) = $window->rect; + + is($absolute->{width}, $rect->[2], "$prefix: width matches"); + is($absolute->{height}, $rect->[3], "$prefix: height matches"); + + is($top->{x}, $rect->[0], "$prefix: x matches"); + is($top->{y}, $rect->[1], "$prefix: y matches"); +} + ############################################################################### # Invalid con_id should not crash i3 # See issue #2895. @@ -261,7 +283,6 @@ sync_with_i3; cmd '[con_mark=B] swap container with mark A'; -sync_with_i3; does_i3_live; $nodes = get_ws_content($ws1); @@ -502,6 +523,142 @@ cmp_float($nodes->[1]->{nodes}->[0]->{percent}, 0.75, 'A has 75% height'); kill_all_windows; +############################################################################### +# Swap floating windows in the same workspace. Check that they exchange rects. +############################################################################### +$ws = fresh_workspace; + +$A = open_floating_window(wm_class => 'mark_A', rect => $rect_A); +$B = open_floating_window(wm_class => 'mark_B', rect => $rect_B); + +cmd '[con_mark=B] swap container with mark A'; +cmp_floating_rect($A, $rect_B, 'A got B\'s rect'); +cmp_floating_rect($B, $rect_A, 'B got A\'s rect'); + +kill_all_windows; + +############################################################################### +# Swap a fullscreen floating and a normal floating window. +############################################################################### +$ws1 = fresh_workspace; +$A = open_floating_window(wm_class => 'mark_A', rect => $rect_A, fullscreen => 1); +$ws2 = fresh_workspace; +$B = open_floating_window(wm_class => 'mark_B', rect => $rect_B); + +cmd '[con_mark=B] swap container with mark A'; + +cmp_floating_rect($A, $rect_B, 'A got B\'s rect'); + +$nodes = get_ws($ws1); +$node = $nodes->{floating_nodes}->[0]->{nodes}->[0]; +is($node->{window}, $B->{id}, 'B is on the first workspace'); +is($node->{fullscreen_mode}, 1, 'B is now fullscreened'); + +$nodes = get_ws($ws2); +$node = $nodes->{floating_nodes}->[0]->{nodes}->[0]; +is($node->{window}, $A->{id}, 'A is on the second workspace'); +is($node->{fullscreen_mode}, 0, 'A is not fullscreened anymore'); + +kill_all_windows; + +############################################################################### +# Swap a floating window which is in a workspace that has another, regular +# window with a regular window in another workspace. A & B focused. +# +# Before: +# +-----------+ +# | F | +# | +---+ | +# | | A | | +# | +---+ | +# | | +# +-----------+ +# +# +-----------+ +# | | +# | | +# | B | +# | | +# | | +# +-----------+ +# +# After: Same as above but A <-> B. A & B focused. +############################################################################### +$ws1 = fresh_workspace; +open_window; +$A = open_floating_window(wm_class => 'mark_A', rect => $rect_A); +$ws2 = fresh_workspace; +$B = open_window(wm_class => 'mark_B'); +$expected_focus = get_focused($ws2); + +cmd '[con_mark=B] swap container with mark A'; + +$nodes = get_ws($ws1); +$node = $nodes->{floating_nodes}->[0]->{nodes}->[0]; +is($node->{window}, $B->{id}, 'B is floating on the first workspace'); +is(get_focused($ws1), $expected_focus, 'B is focused'); +cmp_floating_rect($B, $rect_A, 'B got A\'s rect'); + +$nodes = get_ws_content($ws2); +$node = $nodes->[0]; +is($node->{window}, $A->{id}, 'A is on the second workspace'); + +kill_all_windows; + +################################################################### +# Test that swapping a floating window maintains the correct +# floating order. +################################################################### +$ws = fresh_workspace; +$A = open_floating_window(wm_class => 'mark_A', rect => $rect_A); +$B = open_window(wm_class => 'mark_B'); +$F = open_floating_window; +$expected_focus = get_focused($ws); + +cmd '[con_mark=B] swap container with mark A'; + +$nodes = get_ws($ws); + +$node = $nodes->{floating_nodes}->[0]->{nodes}->[0]; +is($node->{window}, $B->{id}, 'B is floating, bottom'); +cmp_floating_rect($B, $rect_A, 'B got A\'s rect'); + +$node = $nodes->{floating_nodes}->[1]->{nodes}->[0]; +is($node->{window}, $F->{id}, 'F is floating, top'); +is(get_focused($ws), $expected_focus, 'F still focused'); + +$node = $nodes->{nodes}->[0]; +is($node->{window}, $A->{id}, 'A is tiling'); + +kill_all_windows; + +############################################################################### +# Swap a sticky, floating container A and a floating fullscreen container B. +# A should remain sticky and floating and should be fullscreened. +############################################################################### +$ws1 = fresh_workspace; +open_window; +$A = open_floating_window(wm_class => 'mark_A', rect => $rect_A); +$expected_focus = get_focused($ws1); +cmd 'sticky enable'; +$B = open_floating_window(wm_class => 'mark_B', rect => $rect_B); +cmd 'fullscreen enable'; + +cmd '[con_mark=B] swap container with mark A'; + +is(@{get_ws($ws1)->{floating_nodes}}, 2, '2 fullscreen containers on first workspace'); +is(get_focused($ws1), $expected_focus, 'A is focused'); + +$ws2 = fresh_workspace; +cmd 'fullscreen disable'; +cmp_floating_rect($A, $rect_B, 'A got B\'s rect'); +is(@{get_ws($ws2)->{floating_nodes}}, 1, 'only A in new workspace'); + +cmd "workspace $ws1"; # TODO: Why does rect check fails without switching workspaces? +cmp_floating_rect($B, $rect_A, 'B got A\'s rect'); + +kill_all_windows; + ############################################################################### # Swapping containers moves the urgency hint correctly. ############################################################################### @@ -527,6 +684,64 @@ is(get_ws($ws2)->{urgent}, 0, 'the second workspace is not marked urgent'); kill_all_windows; +############################################################################### +# Swapping an urgent container from another workspace with the focused +# container removes the urgency hint from both the container and the previous +# workspace. +############################################################################### + +$ws2 = fresh_workspace; +$B = open_window(wm_class => 'mark_B'); +$ws1 = fresh_workspace; +$A = open_window(wm_class => 'mark_A'); + +$B->add_hint('urgency'); +sync_with_i3; + +cmd '[con_mark=B] swap container with mark A'; + +@urgent = grep { $_->{urgent} } @{get_ws_content($ws1)}; +is(@urgent, 0, 'B is not marked urgent'); +is(get_ws($ws1)->{urgent}, 0, 'the first workspace is not marked urgent'); + +@urgent = grep { $_->{urgent} } @{get_ws_content($ws2)}; +is(@urgent, 0, 'A is not marked urgent'); +is(get_ws($ws2)->{urgent}, 0, 'the second workspace is not marked urgent'); + +kill_all_windows; + +exit_gracefully($pid); + +############################################################################### +# Test that swapping with workspace_layout doesn't crash i3. +# See issue #3280. +############################################################################### + +$config = <[0]->{window}, $B->{id}, 'B is on the left'); + +exit_gracefully($pid); + ############################################################################### done_testing; diff --git a/testcases/t/294-focus-order.t b/testcases/t/294-focus-order.t index c818f1d4..2058e1d8 100644 --- a/testcases/t/294-focus-order.t +++ b/testcases/t/294-focus-order.t @@ -176,6 +176,45 @@ cmd '[id=' . $windows[2]->id . '] move to workspace ' . $ws; cmd '[id=' . $windows[1]->id . '] move to workspace ' . $ws; confirm_focus('\'move to workspace\' focus order when moving containers from other workspace'); +###################################################################### +# Swapping sibling containers correctly swaps focus order. +###################################################################### + +sub swap_with_ids { + my ($idx1, $idx2) = @_; + cmd '[id=' . $windows[$idx1]->id . '] swap container with id ' . $windows[$idx2]->id; +} + +$ws = fresh_workspace; +$windows[2] = open_window; +$windows[0] = open_window; +$windows[3] = open_window; +$windows[1] = open_window; + +# If one of the swapees is focused we deliberately preserve its focus, switch +# workspaces to move focus away from the windows. +fresh_workspace; + +# 2 0 3 1 <- focus order in this direction +swap_with_ids(3, 1); +# 2 0 1 3 +swap_with_ids(2, 3); +# 3 0 1 2 +swap_with_ids(0, 2); +# 3 2 1 0 + +# Restore input focus for confirm_focus +cmd "workspace $ws"; + +# Also confirm swap worked +$ws = get_ws($ws); +for my $i (0 .. 3) { + my $node = $ws->{nodes}->[3 - $i]; + is($node->{window}, $windows[$i]->id, "window $i in correct position after swap"); +} + +confirm_focus('\'swap container with id\' focus order'); + ###################################################################### # Test focus order with floating and tiling windows. # See issue: 1975 From aa203586202981836856d9cf9088620ae866185d Mon Sep 17 00:00:00 2001 From: Albert Safin Date: Fri, 26 Oct 2018 15:40:59 +0700 Subject: [PATCH 06/98] Export I3SOCK (#3476) --- src/startup.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/startup.c b/src/startup.c index 6302d811..c5b7ad5d 100644 --- a/src/startup.c +++ b/src/startup.c @@ -190,6 +190,7 @@ void start_application(const char *command, bool no_startup_id) { /* Setup the environment variable(s) */ if (!no_startup_id) sn_launcher_context_setup_child_process(context); + setenv("I3SOCK", current_socketpath, 1); execl(_PATH_BSHELL, _PATH_BSHELL, "-c", command, NULL); /* not reached */ From 5084a881af4f0baa3b28a2e3417ce383d0425091 Mon Sep 17 00:00:00 2001 From: Hamish Macdonald <44481884+hamishimac@users.noreply.github.com> Date: Fri, 26 Oct 2018 09:31:43 -0400 Subject: [PATCH 07/98] Do not assume STDIN_FILENO is available for input from child --- i3bar/src/child.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/i3bar/src/child.c b/i3bar/src/child.c index 7c527dc3..549a2f70 100644 --- a/i3bar/src/child.c +++ b/i3bar/src/child.c @@ -35,6 +35,7 @@ i3bar_child child; /* stdin- and SIGCHLD-watchers */ ev_io *stdin_io; +int stdin_fd; ev_child *child_sig; /* JSON parser for stdin */ @@ -450,7 +451,7 @@ static void stdin_io_first_line_cb(struct ev_loop *loop, ev_io *watcher, int rev } free(buffer); ev_io_stop(main_loop, stdin_io); - ev_io_init(stdin_io, &stdin_io_cb, STDIN_FILENO, EV_READ); + ev_io_init(stdin_io, &stdin_io_cb, stdin_fd, EV_READ); ev_io_start(main_loop, stdin_io); } @@ -562,17 +563,17 @@ void start_child(char *command) { close(pipe_in[1]); close(pipe_out[0]); - dup2(pipe_in[0], STDIN_FILENO); + stdin_fd = pipe_in[0]; child_stdin = pipe_out[1]; break; } /* We set O_NONBLOCK because blocking is evil in event-driven software */ - fcntl(STDIN_FILENO, F_SETFL, O_NONBLOCK); + fcntl(stdin_fd, F_SETFL, O_NONBLOCK); stdin_io = smalloc(sizeof(ev_io)); - ev_io_init(stdin_io, &stdin_io_first_line_cb, STDIN_FILENO, EV_READ); + ev_io_init(stdin_io, &stdin_io_first_line_cb, stdin_fd, EV_READ); ev_io_start(main_loop, stdin_io); /* We must cleanup, if the child unexpectedly terminates */ From af10b710bf38bc0042b5499eef73a0c3e3492b60 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Sun, 4 Nov 2018 12:21:52 +0200 Subject: [PATCH 08/98] userguide: break long comment --- docs/userguide | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/userguide b/docs/userguide index 91060ab2..c0ab71c7 100644 --- a/docs/userguide +++ b/docs/userguide @@ -816,7 +816,8 @@ assign [class="^URxvt$"] → work # Assign to the workspace with number 2, regardless of name assign [class="^URxvt$"] → number 2 -# You can also specify a number + name. If the workspace with number 2 exists, assign will skip the text part. +# You can also specify a number + name. If the workspace with number 2 exists, +# assign will skip the text part. assign [class="^URxvt$"] → number "2: work" # Start urxvt -name irssi From cb09db94dc7e169bbb49ced9db55211225b3cd08 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sun, 4 Nov 2018 14:55:50 +0100 Subject: [PATCH 09/98] update debian/changelog --- debian/changelog | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/debian/changelog b/debian/changelog index 7695bb4c..f9aa8c9e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,14 @@ -i3-wm (4.15.1-1) unstable; urgency=medium +i3-wm (4.16.1-1) unstable; urgency=medium * UNRELEASED - -- Michael Stapelberg Sat, 10 Mar 2018 17:27:26 +0100 + -- Michael Stapelberg Sun, 04 Nov 2018 14:47:25 +0100 + +i3-wm (4.16-1) unstable; urgency=medium + + * New upstream release. + + -- Michael Stapelberg Sun, 04 Nov 2018 14:47:25 +0100 i3-wm (4.15-1) unstable; urgency=medium From 86bcb21dbc917528783efe04a3501a2b8d5c2461 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sun, 4 Nov 2018 15:07:24 +0100 Subject: [PATCH 10/98] update release.sh for the 4.16 release --- release.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/release.sh b/release.sh index 0190fcfb..4a920ccc 100755 --- a/release.sh +++ b/release.sh @@ -1,8 +1,8 @@ #!/bin/zsh # This script is used to prepare a new release of i3. -export RELEASE_VERSION="4.15" -export PREVIOUS_VERSION="4.14" +export RELEASE_VERSION="4.16" +export PREVIOUS_VERSION="4.15" export RELEASE_BRANCH="next" if [ ! -e "../i3.github.io" ] From 07e5747c8c386cceffd4e1cf9e9dc54f949e1469 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sun, 4 Nov 2018 15:24:10 +0100 Subject: [PATCH 11/98] travis: move (failing) ubuntu build from xenial to bionic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ubuntu’s apt started refusing to load package files from unauthenticated repositories, but the package for which we did that (xcb-xrm) is available in newer versions of Ubuntu, so I just removed that part altogether. Apparently this has been broken since April, and nobody noticed :-/ --- travis/bintray-autobuild-ubuntu.json | 4 ++-- travis/travis-base-ubuntu-386.Dockerfile | 8 ++------ travis/travis-base-ubuntu.Dockerfile | 8 ++------ 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/travis/bintray-autobuild-ubuntu.json b/travis/bintray-autobuild-ubuntu.json index 948f3dc4..37f2f180 100644 --- a/travis/bintray-autobuild-ubuntu.json +++ b/travis/bintray-autobuild-ubuntu.json @@ -15,7 +15,7 @@ { "includePattern": "build/deb/ubuntu-amd64/(.*\\.deb)$", "matrixParams": { - "deb_distribution": "xenial", + "deb_distribution": "bionic", "deb_component": "main", "deb_architecture": "amd64" }, @@ -24,7 +24,7 @@ { "includePattern": "build/deb/ubuntu-i386/(.*\\.deb)$", "matrixParams": { - "deb_distribution": "xenial", + "deb_distribution": "bionic", "deb_component": "main", "deb_architecture": "i386" }, diff --git a/travis/travis-base-ubuntu-386.Dockerfile b/travis/travis-base-ubuntu-386.Dockerfile index 82e5ca29..c71ad42d 100644 --- a/travis/travis-base-ubuntu-386.Dockerfile +++ b/travis/travis-base-ubuntu-386.Dockerfile @@ -1,7 +1,7 @@ # vim:ft=Dockerfile # Same as travis-base.Dockerfile, but without the test suite dependencies since # we only build Debian packages on Ubuntu i386, we don’t run the tests. -FROM i386/ubuntu:xenial +FROM i386/ubuntu:bionic RUN echo force-unsafe-io > /etc/dpkg/dpkg.cfg.d/docker-apt-speedup # Paper over occasional network flakiness of some mirrors. @@ -24,10 +24,6 @@ RUN linux32 apt-get update && \ # Install i3 build dependencies. COPY debian/control /usr/src/i3-debian-packaging/control -RUN echo 'deb http://dl.bintray.com/i3/i3-autobuild-ubuntu xenial main' > /etc/apt/sources.list.d/i3-autobuild.list && \ - linux32 apt-get update && \ - linux32 apt-get --allow-unauthenticated install i3-autobuild-keyring && \ - rm -f /var/lib/apt/lists/dl.bintray.com_i3_i3-autobuild-ubuntu_* && \ - linux32 apt-get update && \ +RUN linux32 apt-get update && \ DEBIAN_FRONTEND=noninteractive mk-build-deps --install --remove --tool 'apt-get --no-install-recommends -y' /usr/src/i3-debian-packaging/control && \ rm -rf /var/lib/apt/lists/* diff --git a/travis/travis-base-ubuntu.Dockerfile b/travis/travis-base-ubuntu.Dockerfile index 05066ec8..6b61e27e 100644 --- a/travis/travis-base-ubuntu.Dockerfile +++ b/travis/travis-base-ubuntu.Dockerfile @@ -1,7 +1,7 @@ # vim:ft=Dockerfile # Same as travis-base.Dockerfile, but without the test suite dependencies since # we only build Debian packages on Ubuntu, we don’t run the tests. -FROM ubuntu:xenial +FROM ubuntu:bionic RUN echo force-unsafe-io > /etc/dpkg/dpkg.cfg.d/docker-apt-speedup # Paper over occasional network flakiness of some mirrors. @@ -25,10 +25,6 @@ RUN apt-get update && \ # Install i3 build dependencies. COPY debian/control /usr/src/i3-debian-packaging/control -RUN echo 'deb http://dl.bintray.com/i3/i3-autobuild-ubuntu xenial main' > /etc/apt/sources.list.d/i3-autobuild.list && \ - apt-get update && \ - apt-get --allow-unauthenticated install i3-autobuild-keyring && \ - rm -f /var/lib/apt/lists/dl.bintray.com_i3_i3-autobuild-ubuntu_* && \ - apt-get update && \ +RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive mk-build-deps --install --remove --tool 'apt-get --no-install-recommends -y' /usr/src/i3-debian-packaging/control && \ rm -rf /var/lib/apt/lists/* From e3ad800902797211ad829d8df4a225999e70f34b Mon Sep 17 00:00:00 2001 From: Morten Linderud Date: Mon, 5 Nov 2018 14:09:01 +0100 Subject: [PATCH 12/98] Change config order in manpage This brings the headline for the configuration files inline with the recent move to XDG directories. Signed-off-by: Morten Linderud --- man/i3.man | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/i3.man b/man/i3.man index 640b5ac8..8157ac77 100644 --- a/man/i3.man +++ b/man/i3.man @@ -166,7 +166,7 @@ Exits i3. == FILES -=== \~/.i3/config (or ~/.config/i3/config) +=== \~/.config/i3/config (or ~/.i3/config) When starting, i3 looks for configuration files in the following order: From 7926c817a2ef0f71e5cda8c3791795e131e22120 Mon Sep 17 00:00:00 2001 From: Albert Safin Date: Sat, 27 Oct 2018 18:32:25 +0700 Subject: [PATCH 13/98] Draw outer header borders for all layouts --- src/x.c | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/src/x.c b/src/x.c index 5b54d145..c0c0961e 100644 --- a/src/x.c +++ b/src/x.c @@ -355,20 +355,22 @@ static void x_draw_title_border(Con *con, struct deco_render_params *p) { assert(con->parent != NULL); Rect *dr = &(con->deco_rect); - adjacent_t borders_to_hide = con_adjacent_borders(con) & config.hide_edge_borders; - int deco_diff_l = borders_to_hide & ADJ_LEFT_SCREEN_EDGE ? 0 : con->current_border_width; - int deco_diff_r = borders_to_hide & ADJ_RIGHT_SCREEN_EDGE ? 0 : con->current_border_width; - if (con->parent->layout == L_TABBED || - (con->parent->layout == L_STACKED && TAILQ_NEXT(con, nodes) != NULL)) { - deco_diff_l = 0; - deco_diff_r = 0; - } + /* Left */ + draw_util_rectangle(&(con->parent->frame_buffer), p->color->border, + dr->x, dr->y, 1, dr->height); + + /* Right */ + draw_util_rectangle(&(con->parent->frame_buffer), p->color->border, + dr->x + dr->width - 1, dr->y, 1, dr->height); + + /* Top */ draw_util_rectangle(&(con->parent->frame_buffer), p->color->border, dr->x, dr->y, dr->width, 1); + /* Bottom */ draw_util_rectangle(&(con->parent->frame_buffer), p->color->border, - dr->x + deco_diff_l, dr->y + dr->height - 1, dr->width - (deco_diff_l + deco_diff_r), 1); + dr->x, dr->y + dr->height - 1, dr->width, 1); } static void x_draw_decoration_after_title(Con *con, struct deco_render_params *p) { @@ -390,18 +392,6 @@ static void x_draw_decoration_after_title(Con *con, struct deco_render_params *p dr->height); } - /* Draw a 1px separator line before and after every tab, so that tabs can - * be easily distinguished. */ - if (con->parent->layout == L_TABBED) { - /* Left side */ - draw_util_rectangle(&(con->parent->frame_buffer), p->color->border, - dr->x, dr->y, 1, dr->height); - - /* Right side */ - draw_util_rectangle(&(con->parent->frame_buffer), p->color->border, - dr->x + dr->width - 1, dr->y, 1, dr->height); - } - /* Redraw the border. */ x_draw_title_border(con, p); } @@ -573,7 +563,7 @@ void x_draw_decoration(Con *con) { draw_util_rectangle(&(parent->frame_buffer), p->color->background, con->deco_rect.x, con->deco_rect.y, con->deco_rect.width, con->deco_rect.height); - /* 5: draw two unconnected horizontal lines in border color */ + /* 5: draw title border */ x_draw_title_border(con, p); /* 6: draw the title */ From 3a3f0f18e60ab63cc90f4d741aa49ff1150a37a6 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Mon, 5 Nov 2018 19:32:29 +0100 Subject: [PATCH 14/98] release.sh: save docs first Otherwise, as @orestisf1993 pointed out, the saved documentation will have the wrong version number. --- release.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/release.sh b/release.sh index 4a920ccc..97d91868 100755 --- a/release.sh +++ b/release.sh @@ -155,6 +155,12 @@ git checkout ${RELEASE_BRANCH} cd ${TMPDIR} git clone --quiet ${STARTDIR}/../i3.github.io cd i3.github.io + +mkdir docs/${PREVIOUS_VERSION} +tar cf - '--exclude=[0-9]\.[0-9e]*' docs | tar xf - --strip-components=1 -C docs/${PREVIOUS_VERSION} +git add docs/${PREVIOUS_VERSION} +git commit -a -m "save docs for ${PREVIOUS_VERSION}" + cp ${TMPDIR}/i3/i3-${RELEASE_VERSION}.tar.bz2* downloads/ git add downloads/i3-${RELEASE_VERSION}.tar.bz2* cp ${TMPDIR}/i3/RELEASE-NOTES-${RELEASE_VERSION} downloads/RELEASE-NOTES-${RELEASE_VERSION}.txt @@ -166,11 +172,6 @@ sed -i "s,,\n \n ${RELEASE_VERSION}\n Date: Wed, 7 Nov 2018 01:06:20 +0200 Subject: [PATCH 15/98] load_layout: Correctly mark non-leaf containers Example problematic layout: { "layout": "splith", "marks": ["H1"], "nodes": [ { "swallows": [ { "class": "^a$" } ] } ] } Since the marks were added to the json_node during end_map, the container that ended up getting the "H1" mark was the child instead of the parent. --- src/load_layout.c | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/load_layout.c b/src/load_layout.c index 5a340d2c..b4d2a688 100644 --- a/src/load_layout.c +++ b/src/load_layout.c @@ -31,7 +31,15 @@ static bool parsing_marks; struct Match *current_swallow; static bool swallow_is_empty; static int num_marks; -static char **marks; +/* We need to save each container that needs to be marked if we want to support + * marking non-leaf containers. In their case, the end_map for their children is + * called before their own end_map, so marking json_node would end up marking + * the latest child. We can't just mark containers immediately after we parse a + * mark because of #2511. */ +struct pending_marks { + char *mark; + Con *con_to_be_marked; +} * marks; /* This list is used for reordering the focus stack after parsing the 'focus' * array. */ @@ -149,8 +157,10 @@ static int json_end_map(void *ctx) { if (num_marks > 0) { for (int i = 0; i < num_marks; i++) { - con_mark(json_node, marks[i], MM_ADD); - free(marks[i]); + Con *con = marks[i].con_to_be_marked; + char *mark = marks[i].mark; + con_mark(con, mark, MM_ADD); + free(mark); } FREE(marks); @@ -274,8 +284,9 @@ static int json_string(void *ctx, const unsigned char *val, size_t len) { char *mark; sasprintf(&mark, "%.*s", (int)len, val); - marks = srealloc(marks, (++num_marks) * sizeof(char *)); - marks[num_marks - 1] = sstrdup(mark); + marks = srealloc(marks, (++num_marks) * sizeof(struct pending_marks)); + marks[num_marks - 1].mark = sstrdup(mark); + marks[num_marks - 1].con_to_be_marked = json_node; } else { if (strcasecmp(last_key, "name") == 0) { json_node->name = scalloc(len + 1, 1); From bbfa140c0fc7c3fbf80619f0c97a4cce50517d27 Mon Sep 17 00:00:00 2001 From: aksel Date: Wed, 7 Nov 2018 22:49:04 +0100 Subject: [PATCH 16/98] For resizing, convert pixel diff to percentage, based on parent. Previously, it first calculated one of the containers' next percentage, and then subtracted the previous percentage to find the actual change. Now it directly calculates the change, and subtracts and adds the change to the two affected containers. Added util function con_rect_size_in_orientation. Removed px_resize_to_percent; inlined, using con_rect_size_in_orientation. Also, prematurely return when pixel diff is 0, as no action is necessary. This is related to [this issue on i3-gaps](https://github.com/Airblader/i3/issues/247). --- include/con.h | 7 +++++++ include/resize.h | 6 ------ src/commands.c | 5 +++-- src/con.c | 9 +++++++++ src/render.c | 2 +- src/resize.c | 33 +++++++++++++-------------------- 6 files changed, 33 insertions(+), 29 deletions(-) diff --git a/include/con.h b/include/con.h index 2c991b0c..88992076 100644 --- a/include/con.h +++ b/include/con.h @@ -533,3 +533,10 @@ i3String *con_parse_title_format(Con *con); * */ bool con_swap(Con *first, Con *second); + +/** + * Returns given container's rect size depending on its orientation. + * i.e. its width when horizontal, its height when vertical. + * + */ +uint32_t con_rect_size_in_orientation(Con *con); diff --git a/include/resize.h b/include/resize.h index 72dffc0f..162d8f6b 100644 --- a/include/resize.h +++ b/include/resize.h @@ -31,9 +31,3 @@ bool resize_neighboring_cons(Con *first, Con *second, int px, int ppt); * */ double percent_for_1px(Con *con); - -/** - * Calculate the given container's new percent given a change in pixels. - * - */ -double px_resize_to_percent(Con *con, int px_diff); diff --git a/src/commands.c b/src/commands.c index eecd59fc..57dc58b4 100644 --- a/src/commands.c +++ b/src/commands.c @@ -537,8 +537,9 @@ static bool cmd_resize_tiling_width_height(I3_CMD, Con *current, const char *dir if (ppt != 0.0) { new_current_percent = current->percent + ppt; } else { - new_current_percent = px_resize_to_percent(current, px); - ppt = new_current_percent - current->percent; + /* Convert px change to change in percentages */ + ppt = (double)px / (double)con_rect_size_in_orientation(current->parent); + new_current_percent = current->percent + ppt; } subtract_percent = ppt / (children - 1); if (ppt < 0.0 && new_current_percent < percent_for_1px(current)) { diff --git a/src/con.c b/src/con.c index 519bb8fd..51c2c48b 100644 --- a/src/con.c +++ b/src/con.c @@ -2430,3 +2430,12 @@ bool con_swap(Con *first, Con *second) { return true; } + +/* + * Returns container's rect size depending on its orientation. + * i.e. its width when horizontal, its height when vertical. + * + */ +uint32_t con_rect_size_in_orientation(Con *con) { + return (con_orientation(con) == HORIZ ? con->rect.width : con->rect.height); +} diff --git a/src/render.c b/src/render.c index d8bffc61..8ea21f27 100644 --- a/src/render.c +++ b/src/render.c @@ -192,7 +192,7 @@ static int *precalculate_sizes(Con *con, render_params *p) { Con *child; int i = 0, assigned = 0; - int total = con_orientation(con) == HORIZ ? p->rect.width : p->rect.height; + int total = con_rect_size_in_orientation(con); TAILQ_FOREACH(child, &(con->nodes_head), nodes) { double percentage = child->percent > 0.0 ? child->percent : 1.0 / p->children; assigned += sizes[i++] = lround(percentage * total); diff --git a/src/resize.c b/src/resize.c index d746ea22..5ddee5c1 100644 --- a/src/resize.c +++ b/src/resize.c @@ -101,30 +101,16 @@ bool resize_find_tiling_participants(Con **current, Con **other, direction_t dir return true; } -/* - * Calculate the given container's new percent given a change in pixels. - * - */ -double px_resize_to_percent(Con *con, int px_diff) { - Con *parent = con->parent; - const orientation_t o = con_orientation(parent); - const int total = (o == HORIZ ? parent->rect.width : parent->rect.height); - /* deco_rect.height is subtracted from each child in render_con_split */ - const int target = px_diff + (o == HORIZ ? con->rect.width : con->rect.height + con->deco_rect.height); - return ((double)target / (double)total); -} - /* * Calculate the minimum percent needed for the given container to be at least 1 * pixel. * */ double percent_for_1px(Con *con) { - Con *parent = con->parent; - const orientation_t o = con_orientation(parent); - const int total = (o == HORIZ ? parent->rect.width : parent->rect.height); - const int target = (o == HORIZ ? 1 : 1 + con->deco_rect.height); - return ((double)target / (double)total); + const int parent_size = con_rect_size_in_orientation(con->parent); + /* deco_rect.height is subtracted from each child in render_con_split */ + const int min_size = (con_orientation(con->parent) == HORIZ ? 1 : 1 + con->deco_rect.height); + return ((double)min_size / (double)parent_size); } /* @@ -145,8 +131,10 @@ bool resize_neighboring_cons(Con *first, Con *second, int px, int ppt) { new_first_percent = first->percent + ((double)ppt / 100.0); new_second_percent = second->percent - ((double)ppt / 100.0); } else { - new_first_percent = px_resize_to_percent(first, px); - new_second_percent = second->percent + first->percent - new_first_percent; + /* Convert px change to change in percentages */ + const double pct = (double)px / (double)con_rect_size_in_orientation(first->parent); + new_first_percent = first->percent + pct; + new_second_percent = second->percent - pct; } /* Ensure that no container will be less than 1 pixel in the resizing * direction. */ @@ -234,6 +222,11 @@ void resize_graphical_handler(Con *first, Con *second, orientation_t orientation int pixels = (new_position - initial_position); DLOG("Done, pixels = %d\n", pixels); + /* No change; no action needed. */ + if (pixels == 0) { + return; + } + /* if we got thus far, the containers must have valid percentages. */ assert(first->percent > 0.0); assert(second->percent > 0.0); From 11bc25b70b76cd8e363443f8d4b8326f5db5a4c4 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Fri, 9 Nov 2018 19:41:31 +0200 Subject: [PATCH 17/98] Truncate wm_name utf8 strings to first zero byte Fixes #3515 --- src/window.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/window.c b/src/window.c index 68c6933a..61282556 100644 --- a/src/window.c +++ b/src/window.c @@ -74,8 +74,12 @@ void window_update_name(i3Window *win, xcb_get_property_reply_t *prop, bool befo } i3string_free(win->name); - win->name = i3string_from_utf8_with_length(xcb_get_property_value(prop), - xcb_get_property_value_length(prop)); + + /* Truncate the name at the first zero byte. See #3515. */ + const int len = xcb_get_property_value_length(prop); + char *name = sstrndup(xcb_get_property_value(prop), len); + win->name = i3string_from_utf8(name); + free(name); Con *con = con_by_window_id(win->id); if (con != NULL && con->title_format != NULL) { @@ -119,8 +123,10 @@ void window_update_name_legacy(i3Window *win, xcb_get_property_reply_t *prop, bo } i3string_free(win->name); - win->name = i3string_from_utf8_with_length(xcb_get_property_value(prop), - xcb_get_property_value_length(prop)); + const int len = xcb_get_property_value_length(prop); + char *name = sstrndup(xcb_get_property_value(prop), len); + win->name = i3string_from_utf8(name); + free(name); Con *con = con_by_window_id(win->id); if (con != NULL && con->title_format != NULL) { From 01960f956f498ee81124e80d96bdc1e521518583 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Mon, 1 Oct 2018 15:42:53 +0300 Subject: [PATCH 18/98] floating_check_size: Use window variable --- src/floating.c | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/floating.c b/src/floating.c index a99d0970..c9600eac 100644 --- a/src/floating.c +++ b/src/floating.c @@ -83,43 +83,44 @@ void floating_check_size(Con *floating_con) { border_rect.height += render_deco_height(); } - if (focused_con->window != NULL) { - if (focused_con->window->min_width) { + i3Window *window = focused_con->window; + if (window != NULL) { + if (window->min_width) { floating_con->rect.width -= border_rect.width; - floating_con->rect.width = max(floating_con->rect.width, focused_con->window->min_width); + floating_con->rect.width = max(floating_con->rect.width, window->min_width); floating_con->rect.width += border_rect.width; } - if (focused_con->window->min_height) { + if (window->min_height) { floating_con->rect.height -= border_rect.height; - floating_con->rect.height = max(floating_con->rect.height, focused_con->window->min_height); + floating_con->rect.height = max(floating_con->rect.height, window->min_height); floating_con->rect.height += border_rect.height; } - if (focused_con->window->max_width) { + if (window->max_width) { floating_con->rect.width -= border_rect.width; - floating_con->rect.width = min(floating_con->rect.width, focused_con->window->max_width); + floating_con->rect.width = min(floating_con->rect.width, window->max_width); floating_con->rect.width += border_rect.width; } - if (focused_con->window->max_height) { + if (window->max_height) { floating_con->rect.height -= border_rect.height; - floating_con->rect.height = min(floating_con->rect.height, focused_con->window->max_height); + floating_con->rect.height = min(floating_con->rect.height, window->max_height); floating_con->rect.height += border_rect.height; } - if (focused_con->window->height_increment && - floating_con->rect.height >= focused_con->window->base_height + border_rect.height) { - floating_con->rect.height -= focused_con->window->base_height + border_rect.height; - floating_con->rect.height -= floating_con->rect.height % focused_con->window->height_increment; - floating_con->rect.height += focused_con->window->base_height + border_rect.height; + if (window->height_increment && + floating_con->rect.height >= window->base_height + border_rect.height) { + floating_con->rect.height -= window->base_height + border_rect.height; + floating_con->rect.height -= floating_con->rect.height % window->height_increment; + floating_con->rect.height += window->base_height + border_rect.height; } - if (focused_con->window->width_increment && - floating_con->rect.width >= focused_con->window->base_width + border_rect.width) { - floating_con->rect.width -= focused_con->window->base_width + border_rect.width; - floating_con->rect.width -= floating_con->rect.width % focused_con->window->width_increment; - floating_con->rect.width += focused_con->window->base_width + border_rect.width; + if (window->width_increment && + floating_con->rect.width >= window->base_width + border_rect.width) { + floating_con->rect.width -= window->base_width + border_rect.width; + floating_con->rect.width -= floating_con->rect.width % window->width_increment; + floating_con->rect.width += window->base_width + border_rect.width; } } From f397698d43e3dd4f4625e1fed1b1d0470d0acdf6 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Tue, 2 Oct 2018 01:52:31 +0300 Subject: [PATCH 19/98] floating_resize: Use uint32_t --- include/floating.h | 2 +- src/floating.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/floating.h b/include/floating.h index 4382437b..368183bc 100644 --- a/include/floating.h +++ b/include/floating.h @@ -152,7 +152,7 @@ bool floating_reposition(Con *con, Rect newrect); * window's size hints. * */ -void floating_resize(Con *floating_con, int x, int y); +void floating_resize(Con *floating_con, uint32_t x, uint32_t y); /** * Fixes the coordinates of the floating window whenever the window gets diff --git a/src/floating.c b/src/floating.c index c9600eac..0c7b43b2 100644 --- a/src/floating.c +++ b/src/floating.c @@ -921,7 +921,7 @@ bool floating_reposition(Con *con, Rect newrect) { * window's size hints. * */ -void floating_resize(Con *floating_con, int x, int y) { +void floating_resize(Con *floating_con, uint32_t x, uint32_t y) { DLOG("floating resize to %dx%d px\n", x, y); Rect *rect = &floating_con->rect; Con *focused_con = con_descend_focused(floating_con); From 29f2510fa9484a5fa235de9a1c5c937292151888 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Mon, 1 Oct 2018 19:47:33 +0300 Subject: [PATCH 20/98] Fix aspect ratio bugs - ICCCM says: > If a base size is not provided, the minimum size is to be used in its place and vice versa. i3 didn't obey the "vice versa" part. Min size and base size are both saved without replacements in window_update_normal_hints, floating_check_size makes the needed replacements if either was not provided. - Aspect ratio is now saved correctly in manage_window because window_update_normal_hints is called. - i3 didn't save the aspect ratio if the window conformed the given aspect ratio range when handle_normal_hints was called. If the window was resized to a size outside of the given bounds, i3 didn't correct it. - Aspect ratio now affects only tiling windows, like the rest of the normal size hints - The aspect ratio calculation is now done without a loop A real life example of how these changes affect the workflow: An mpv window, when playing a video, sets its min == max aspect ratio during mapping. i3 ignored these hints. When resized, the window's aspect ratio was not preserved. With this commit, resizing floating mpv windows will always preserve the aspect ratio. --- include/data.h | 3 +- include/floating.h | 13 ++-- include/window.h | 6 ++ src/commands.c | 2 +- src/con.c | 1 - src/floating.c | 85 ++++++++++++++++++----- src/handlers.c | 130 ++--------------------------------- src/load_layout.c | 2 +- src/manage.c | 34 ++------- src/render.c | 29 -------- src/scratchpad.c | 2 +- src/window.c | 121 ++++++++++++++++++++++++++++++++ testcases/t/133-size-hints.t | 104 +++++++++++++++++++++++----- 13 files changed, 304 insertions(+), 228 deletions(-) diff --git a/include/data.h b/include/data.h index f55e003d..d2b501b9 100644 --- a/include/data.h +++ b/include/data.h @@ -482,7 +482,8 @@ struct Window { int max_height; /* aspect ratio from WM_NORMAL_HINTS (MPlayer uses this for example) */ - double aspect_ratio; + double min_aspect_ratio; + double max_aspect_ratio; }; /** diff --git a/include/floating.h b/include/floating.h index 368183bc..a7813099 100644 --- a/include/floating.h +++ b/include/floating.h @@ -94,12 +94,17 @@ void floating_drag_window(Con *con, const xcb_button_press_event_t *event); void floating_resize_window(Con *con, const bool proportional, const xcb_button_press_event_t *event); /** - * Called when a floating window is created or resized. - * This function resizes the window if its size is higher or lower than the - * configured maximum/minimum size, respectively. + * Called when a floating window is created or resized. This function resizes + * the window if its size is higher or lower than the configured maximum/minimum + * size, respectively or when adjustments are needed to conform to the + * configured size increments or aspect ratio limits. + * + * When prefer_height is true and the window needs to be resized because of the + * configured aspect ratio, the width is adjusted first, preserving the previous + * height. * */ -void floating_check_size(Con *floating_con); +void floating_check_size(Con *floating_con, bool prefer_height); /** * This is the return value of a drag operation like drag_pointer. diff --git a/include/window.h b/include/window.h index 77e3f48f..b03f9c14 100644 --- a/include/window.h +++ b/include/window.h @@ -70,6 +70,12 @@ void window_update_role(i3Window *win, xcb_get_property_reply_t *prop, bool befo */ void window_update_type(i3Window *window, xcb_get_property_reply_t *reply); +/** + * Updates the WM_NORMAL_HINTS + * + */ +bool window_update_normal_hints(i3Window *win, xcb_get_property_reply_t *reply, xcb_get_geometry_reply_t *geom); + /** * Updates the WM_HINTS (we only care about the input focus handling part). * diff --git a/src/commands.c b/src/commands.c index 57dc58b4..ee402846 100644 --- a/src/commands.c +++ b/src/commands.c @@ -467,7 +467,7 @@ static void cmd_resize_floating(I3_CMD, const char *way, const char *direction_s } else { floating_con->rect.width += px; } - floating_check_size(floating_con); + floating_check_size(floating_con, orientation == VERT); /* Did we actually resize anything or did the size constraints prevent us? * If we could not resize, exit now to not move the window. */ diff --git a/src/con.c b/src/con.c index 51c2c48b..764419dc 100644 --- a/src/con.c +++ b/src/con.c @@ -46,7 +46,6 @@ Con *con_new_skeleton(Con *parent, i3Window *window) { new->current_border_width = -1; if (window) { new->depth = window->depth; - new->window->aspect_ratio = 0.0; } else { new->depth = root_depth; } diff --git a/src/floating.c b/src/floating.c index 0c7b43b2..4f2760a7 100644 --- a/src/floating.c +++ b/src/floating.c @@ -60,12 +60,17 @@ static void floating_set_hint_atom(Con *con, bool floating) { } /* - * Called when a floating window is created or resized. - * This function resizes the window if its size is higher or lower than the - * configured maximum/minimum size, respectively. + * Called when a floating window is created or resized. This function resizes + * the window if its size is higher or lower than the configured maximum/minimum + * size, respectively or when adjustments are needed to conform to the + * configured size increments or aspect ratio limits. + * + * When prefer_height is true and the window needs to be resized because of the + * configured aspect ratio, the width is adjusted first, preserving the previous + * height. * */ -void floating_check_size(Con *floating_con) { +void floating_check_size(Con *floating_con, bool prefer_height) { /* Define reasonable minimal and maximal sizes for floating windows */ const int floating_sane_min_height = 50; const int floating_sane_min_width = 75; @@ -85,15 +90,22 @@ void floating_check_size(Con *floating_con) { i3Window *window = focused_con->window; if (window != NULL) { - if (window->min_width) { + /* ICCCM says: If a base size is not provided, the minimum size is to be used in its place + * and vice versa. */ + int min_width = (window->min_width ? window->min_width : window->base_width); + int min_height = (window->min_height ? window->min_height : window->base_height); + int base_width = (window->base_width ? window->base_width : window->min_width); + int base_height = (window->base_height ? window->base_height : window->min_height); + + if (min_width) { floating_con->rect.width -= border_rect.width; - floating_con->rect.width = max(floating_con->rect.width, window->min_width); + floating_con->rect.width = max(floating_con->rect.width, min_width); floating_con->rect.width += border_rect.width; } - if (window->min_height) { + if (min_height) { floating_con->rect.height -= border_rect.height; - floating_con->rect.height = max(floating_con->rect.height, window->min_height); + floating_con->rect.height = max(floating_con->rect.height, min_height); floating_con->rect.height += border_rect.height; } @@ -109,18 +121,56 @@ void floating_check_size(Con *floating_con) { floating_con->rect.height += border_rect.height; } + /* Obey the aspect ratio, if any, unless we are in fullscreen mode. + * + * The spec isn’t explicit on whether the aspect ratio hints should be + * respected during fullscreen mode. Other WMs such as Openbox don’t do + * that, and this post suggests that this is the correct way to do it: + * https://mail.gnome.org/archives/wm-spec-list/2003-May/msg00007.html + * + * Ignoring aspect ratio during fullscreen was necessary to fix MPlayer + * subtitle rendering, see https://bugs.i3wm.org/594 */ + const double min_ar = window->min_aspect_ratio; + const double max_ar = window->max_aspect_ratio; + if (floating_con->fullscreen_mode == CF_NONE && (min_ar > 0 || max_ar > 0)) { + /* The ICCCM says to subtract the base size from the window size for + * aspect ratio calculations. However, unlike determining the base + * size itself we must not fall back to using the minimum size in + * this case according to the ICCCM. */ + double width = floating_con->rect.width - window->base_width - border_rect.width; + double height = floating_con->rect.height - window->base_height - border_rect.height; + const double ar = (double)width / (double)height; + double new_ar = -1; + if (min_ar > 0 && ar < min_ar) { + new_ar = min_ar; + } else if (max_ar > 0 && ar > max_ar) { + new_ar = max_ar; + } + if (new_ar > 0) { + if (prefer_height) { + width = round(height * new_ar); + height = round(width / new_ar); + } else { + height = round(width / new_ar); + width = round(height * new_ar); + } + floating_con->rect.width = width + window->base_width + border_rect.width; + floating_con->rect.height = height + window->base_height + border_rect.height; + } + } + if (window->height_increment && - floating_con->rect.height >= window->base_height + border_rect.height) { - floating_con->rect.height -= window->base_height + border_rect.height; + floating_con->rect.height >= base_height + border_rect.height) { + floating_con->rect.height -= base_height + border_rect.height; floating_con->rect.height -= floating_con->rect.height % window->height_increment; - floating_con->rect.height += window->base_height + border_rect.height; + floating_con->rect.height += base_height + border_rect.height; } if (window->width_increment && - floating_con->rect.width >= window->base_width + border_rect.width) { - floating_con->rect.width -= window->base_width + border_rect.width; + floating_con->rect.width >= base_width + border_rect.width) { + floating_con->rect.width -= base_width + border_rect.width; floating_con->rect.width -= floating_con->rect.width % window->width_increment; - floating_con->rect.width += window->base_width + border_rect.width; + floating_con->rect.width += base_width + border_rect.width; } } @@ -314,7 +364,7 @@ void floating_enable(Con *con, bool automatic) { nc->rect.height += con->border_width * 2; nc->rect.width += con->border_width * 2; - floating_check_size(nc); + floating_check_size(nc, false); /* Some clients (like GIMP’s color picker window) get mapped * to (0, 0), so we push them to a reasonable position @@ -612,7 +662,7 @@ DRAGGING_CB(resize_window_callback) { con->rect = (Rect){dest_x, dest_y, dest_width, dest_height}; /* Obey window size */ - floating_check_size(con); + floating_check_size(con, false); /* If not the lower right corner is grabbed, we must also reposition * the client by exactly the amount we resized it */ @@ -931,6 +981,7 @@ void floating_resize(Con *floating_con, uint32_t x, uint32_t y) { } int wi = focused_con->window->width_increment; int hi = focused_con->window->height_increment; + bool prefer_height = (rect->width == x); rect->width = x; rect->height = y; if (wi) @@ -938,7 +989,7 @@ void floating_resize(Con *floating_con, uint32_t x, uint32_t y) { if (hi) rect->height += (hi - 1 - rect->height) % hi; - floating_check_size(floating_con); + floating_check_size(floating_con, prefer_height); /* If this is a scratchpad window, don't auto center it from now on. */ if (floating_con->scratchpad_state == SCRATCHPAD_FRESH) diff --git a/src/handlers.c b/src/handlers.c index b9677917..c4b8cd08 100644 --- a/src/handlers.c +++ b/src/handlers.c @@ -974,132 +974,14 @@ static bool handle_normal_hints(void *data, xcb_connection_t *conn, uint8_t stat return false; } - xcb_size_hints_t size_hints; + bool changed = window_update_normal_hints(con->window, reply, NULL); - /* If the hints were already in this event, use them, if not, request them */ - if (reply != NULL) { - xcb_icccm_get_wm_size_hints_from_reply(&size_hints, reply); - } else { - xcb_icccm_get_wm_normal_hints_reply(conn, xcb_icccm_get_wm_normal_hints_unchecked(conn, con->window->id), &size_hints, NULL); - } - - int win_width = con->window_rect.width; - int win_height = con->window_rect.height; - - if ((size_hints.flags & XCB_ICCCM_SIZE_HINT_P_MIN_SIZE)) { - DLOG("Minimum size: %d (width) x %d (height)\n", size_hints.min_width, size_hints.min_height); - - con->window->min_width = size_hints.min_width; - con->window->min_height = size_hints.min_height; - } - - if ((size_hints.flags & XCB_ICCCM_SIZE_HINT_P_MAX_SIZE)) { - DLOG("Maximum size: %d (width) x %d (height)\n", size_hints.max_width, size_hints.max_height); - - con->window->max_width = size_hints.max_width; - con->window->max_height = size_hints.max_height; - } - - if (con_is_floating(con)) { - win_width = MAX(win_width, con->window->min_width); - win_height = MAX(win_height, con->window->min_height); - win_width = MIN(win_width, con->window->max_width); - win_height = MIN(win_height, con->window->max_height); - } - - bool changed = false; - if ((size_hints.flags & XCB_ICCCM_SIZE_HINT_P_RESIZE_INC)) { - if (size_hints.width_inc > 0 && size_hints.width_inc < 0xFFFF) { - if (con->window->width_increment != size_hints.width_inc) { - con->window->width_increment = size_hints.width_inc; - changed = true; - } - } - - if (size_hints.height_inc > 0 && size_hints.height_inc < 0xFFFF) { - if (con->window->height_increment != size_hints.height_inc) { - con->window->height_increment = size_hints.height_inc; - changed = true; - } - } - - if (changed) { - DLOG("resize increments changed\n"); - } - } - - bool has_base_size = false; - int base_width = 0; - int base_height = 0; - - /* The base width / height is the desired size of the window. */ - if (size_hints.flags & XCB_ICCCM_SIZE_HINT_BASE_SIZE) { - base_width = size_hints.base_width; - base_height = size_hints.base_height; - has_base_size = true; - } - - /* If the window didn't specify a base size, the ICCCM tells us to fall - * back to the minimum size instead, if available. */ - if (!has_base_size && size_hints.flags & XCB_ICCCM_SIZE_HINT_P_MIN_SIZE) { - base_width = size_hints.min_width; - base_height = size_hints.min_height; - } - - // TODO XXX Should we only do this is the base size is > 0? - if (base_width != con->window->base_width || base_height != con->window->base_height) { - con->window->base_width = base_width; - con->window->base_height = base_height; - - DLOG("client's base_height changed to %d\n", base_height); - DLOG("client's base_width changed to %d\n", base_width); - changed = true; - } - - /* If no aspect ratio was set or if it was invalid, we ignore the hints */ - if (!(size_hints.flags & XCB_ICCCM_SIZE_HINT_P_ASPECT) || - (size_hints.min_aspect_num <= 0) || - (size_hints.min_aspect_den <= 0)) { - goto render_and_return; - } - - /* The ICCCM says to subtract the base size from the window size for aspect - * ratio calculations. However, unlike determining the base size itself we - * must not fall back to using the minimum size in this case according to - * the ICCCM. */ - double width = win_width - base_width * has_base_size; - double height = win_height - base_height * has_base_size; - - /* Convert numerator/denominator to a double */ - double min_aspect = (double)size_hints.min_aspect_num / size_hints.min_aspect_den; - double max_aspect = (double)size_hints.max_aspect_num / size_hints.max_aspect_den; - - DLOG("Aspect ratio set: minimum %f, maximum %f\n", min_aspect, max_aspect); - DLOG("width = %f, height = %f\n", width, height); - - /* Sanity checks, this is user-input, in a way */ - if (max_aspect <= 0 || min_aspect <= 0 || height == 0 || (width / height) <= 0) { - goto render_and_return; - } - - /* Check if we need to set proportional_* variables using the correct ratio */ - double aspect_ratio = 0.0; - if ((width / height) < min_aspect) { - aspect_ratio = min_aspect; - } else if ((width / height) > max_aspect) { - aspect_ratio = max_aspect; - } else { - goto render_and_return; - } - - if (fabs(con->window->aspect_ratio - aspect_ratio) > DBL_EPSILON) { - con->window->aspect_ratio = aspect_ratio; - changed = true; - } - -render_and_return: if (changed) { - tree_render(); + Con *floating = con_inside_floating(con); + if (floating) { + floating_check_size(con, false); + tree_render(); + } } FREE(reply); diff --git a/src/load_layout.c b/src/load_layout.c index b4d2a688..003affee 100644 --- a/src/load_layout.c +++ b/src/load_layout.c @@ -152,7 +152,7 @@ static int json_end_map(void *ctx) { } } - floating_check_size(json_node); + floating_check_size(json_node, false); } if (num_marks > 0) { diff --git a/src/manage.c b/src/manage.c index c4706b0d..c222d351 100644 --- a/src/manage.c +++ b/src/manage.c @@ -185,9 +185,7 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki window_update_hints(cwindow, xcb_get_property_reply(conn, wm_hints_cookie, NULL), &urgency_hint); border_style_t motif_border_style = BS_NORMAL; window_update_motif_hints(cwindow, xcb_get_property_reply(conn, motif_wm_hints_cookie, NULL), &motif_border_style); - xcb_size_hints_t wm_size_hints; - if (!xcb_icccm_get_wm_size_hints_reply(conn, wm_normal_hints_cookie, &wm_size_hints, NULL)) - memset(&wm_size_hints, '\0', sizeof(xcb_size_hints_t)); + window_update_normal_hints(cwindow, xcb_get_property_reply(conn, wm_normal_hints_cookie, NULL), geom); xcb_get_property_reply_t *type_reply = xcb_get_property_reply(conn, wm_type_cookie, NULL); xcb_get_property_reply_t *state_reply = xcb_get_property_reply(conn, state_cookie, NULL); @@ -437,10 +435,9 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki xcb_reply_contains_atom(type_reply, A__NET_WM_WINDOW_TYPE_TOOLBAR) || xcb_reply_contains_atom(type_reply, A__NET_WM_WINDOW_TYPE_SPLASH) || xcb_reply_contains_atom(state_reply, A__NET_WM_STATE_MODAL) || - (wm_size_hints.flags & XCB_ICCCM_SIZE_HINT_P_MAX_SIZE && - wm_size_hints.flags & XCB_ICCCM_SIZE_HINT_P_MIN_SIZE && - wm_size_hints.min_height == wm_size_hints.max_height && - wm_size_hints.min_width == wm_size_hints.max_width)) { + (cwindow->max_width > 0 && cwindow->max_height > 0 && + cwindow->min_height == cwindow->max_height && + cwindow->min_width == cwindow->max_width)) { LOG("This window is a dialog window, setting floating\n"); want_floating = true; } @@ -499,29 +496,6 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki if (cwindow->dock) want_floating = false; - /* Plasma windows set their geometry in WM_SIZE_HINTS. */ - if ((wm_size_hints.flags & XCB_ICCCM_SIZE_HINT_US_POSITION || wm_size_hints.flags & XCB_ICCCM_SIZE_HINT_P_POSITION) && - (wm_size_hints.flags & XCB_ICCCM_SIZE_HINT_US_SIZE || wm_size_hints.flags & XCB_ICCCM_SIZE_HINT_P_SIZE)) { - DLOG("We are setting geometry according to wm_size_hints x=%d y=%d w=%d h=%d\n", - wm_size_hints.x, wm_size_hints.y, wm_size_hints.width, wm_size_hints.height); - geom->x = wm_size_hints.x; - geom->y = wm_size_hints.y; - geom->width = wm_size_hints.width; - geom->height = wm_size_hints.height; - } - - if (wm_size_hints.flags & XCB_ICCCM_SIZE_HINT_P_MIN_SIZE) { - DLOG("Window specifies minimum size %d x %d\n", wm_size_hints.min_width, wm_size_hints.min_height); - nc->window->min_width = wm_size_hints.min_width; - nc->window->min_height = wm_size_hints.min_height; - } - - if (wm_size_hints.flags & XCB_ICCCM_SIZE_HINT_P_MAX_SIZE) { - DLOG("Window specifies maximum size %d x %d\n", wm_size_hints.max_width, wm_size_hints.max_height); - nc->window->max_width = wm_size_hints.max_width; - nc->window->max_height = wm_size_hints.max_height; - } - /* Store the requested geometry. The width/height gets raised to at least * 75x50 when entering floating mode, which is the minimum size for a * window to be useful (smaller windows are usually overlays/toolbars/… diff --git a/src/render.c b/src/render.c index 8ea21f27..518d436b 100644 --- a/src/render.c +++ b/src/render.c @@ -64,35 +64,6 @@ void render_con(Con *con, bool render_fullscreen) { inset->width -= (2 * con->border_width); inset->height -= (2 * con->border_width); - /* Obey the aspect ratio, if any, unless we are in fullscreen mode. - * - * The spec isn’t explicit on whether the aspect ratio hints should be - * respected during fullscreen mode. Other WMs such as Openbox don’t do - * that, and this post suggests that this is the correct way to do it: - * https://mail.gnome.org/archives/wm-spec-list/2003-May/msg00007.html - * - * Ignoring aspect ratio during fullscreen was necessary to fix MPlayer - * subtitle rendering, see https://bugs.i3wm.org/594 */ - if (!render_fullscreen && con->window->aspect_ratio > 0.0) { - DLOG("aspect_ratio = %f, current width/height are %d/%d\n", - con->window->aspect_ratio, inset->width, inset->height); - double new_height = inset->height + 1; - int new_width = inset->width; - - while (new_height > inset->height) { - new_height = (1.0 / con->window->aspect_ratio) * new_width; - - if (new_height > inset->height) - new_width--; - } - /* Center the window */ - inset->y += ceil(inset->height / 2) - floor((new_height + .5) / 2); - inset->x += ceil(inset->width / 2) - floor(new_width / 2); - - inset->height = new_height + .5; - inset->width = new_width; - } - /* NB: We used to respect resize increment size hints for tiling * windows up until commit 0db93d9 here. However, since all terminal * emulators cope with ignoring the size hints in a better way than we diff --git a/src/scratchpad.c b/src/scratchpad.c index d564bf32..a679f20a 100644 --- a/src/scratchpad.c +++ b/src/scratchpad.c @@ -196,7 +196,7 @@ bool scratchpad_show(Con *con) { Con *output = con_get_output(con); con->rect.width = output->rect.width * 0.5; con->rect.height = output->rect.height * 0.75; - floating_check_size(con); + floating_check_size(con, false); floating_center(con, con_get_workspace(con)->rect); } diff --git a/src/window.c b/src/window.c index 61282556..bec4c691 100644 --- a/src/window.c +++ b/src/window.c @@ -272,6 +272,127 @@ void window_update_type(i3Window *window, xcb_get_property_reply_t *reply) { run_assignments(window); } +/* + * Updates the WM_NORMAL_HINTS + * + */ +bool window_update_normal_hints(i3Window *win, xcb_get_property_reply_t *reply, xcb_get_geometry_reply_t *geom) { + bool changed = false; + xcb_size_hints_t size_hints; + + /* If the hints were already in this event, use them, if not, request them */ + bool success; + if (reply != NULL) { + success = xcb_icccm_get_wm_size_hints_from_reply(&size_hints, reply); + } else { + success = xcb_icccm_get_wm_normal_hints_reply(conn, xcb_icccm_get_wm_normal_hints_unchecked(conn, win->id), &size_hints, NULL); + } + if (!success) { + DLOG("Could not get WM_NORMAL_HINTS\n"); + return false; + } + +#define ASSIGN_IF_CHANGED(original, new) \ + do { \ + if (original != new) { \ + original = new; \ + changed = true; \ + } \ + } while (0) + + if ((size_hints.flags & XCB_ICCCM_SIZE_HINT_P_MIN_SIZE)) { + DLOG("Minimum size: %d (width) x %d (height)\n", size_hints.min_width, size_hints.min_height); + + ASSIGN_IF_CHANGED(win->min_width, size_hints.min_width); + ASSIGN_IF_CHANGED(win->min_height, size_hints.min_height); + } + + if ((size_hints.flags & XCB_ICCCM_SIZE_HINT_P_MAX_SIZE)) { + DLOG("Maximum size: %d (width) x %d (height)\n", size_hints.max_width, size_hints.max_height); + + int max_width = max(0, size_hints.max_width); + int max_height = max(0, size_hints.max_height); + + ASSIGN_IF_CHANGED(win->max_width, max_width); + ASSIGN_IF_CHANGED(win->max_height, max_height); + } else { + DLOG("Clearing maximum size \n"); + + ASSIGN_IF_CHANGED(win->max_width, 0); + ASSIGN_IF_CHANGED(win->max_height, 0); + } + + if ((size_hints.flags & XCB_ICCCM_SIZE_HINT_P_RESIZE_INC)) { + DLOG("Size increments: %d (width) x %d (height)\n", size_hints.width_inc, size_hints.height_inc); + + if (size_hints.width_inc > 0 && size_hints.width_inc < 0xFFFF) { + ASSIGN_IF_CHANGED(win->width_increment, size_hints.width_inc); + } else { + ASSIGN_IF_CHANGED(win->width_increment, 0); + } + + if (size_hints.height_inc > 0 && size_hints.height_inc < 0xFFFF) { + ASSIGN_IF_CHANGED(win->height_increment, size_hints.height_inc); + } else { + ASSIGN_IF_CHANGED(win->height_increment, 0); + } + } else { + DLOG("Clearing size increments\n"); + + ASSIGN_IF_CHANGED(win->width_increment, 0); + ASSIGN_IF_CHANGED(win->height_increment, 0); + } + + /* The base width / height is the desired size of the window. */ + if (size_hints.flags & XCB_ICCCM_SIZE_HINT_BASE_SIZE && + (win->base_width >= 0) && (win->base_height >= 0)) { + DLOG("Base size: %d (width) x %d (height)\n", size_hints.base_width, size_hints.base_height); + + ASSIGN_IF_CHANGED(win->base_width, size_hints.base_width); + ASSIGN_IF_CHANGED(win->base_height, size_hints.base_height); + } else { + DLOG("Clearing base size\n"); + + ASSIGN_IF_CHANGED(win->base_width, 0); + ASSIGN_IF_CHANGED(win->base_height, 0); + } + + if (geom != NULL && + (size_hints.flags & XCB_ICCCM_SIZE_HINT_US_POSITION || size_hints.flags & XCB_ICCCM_SIZE_HINT_P_POSITION) && + (size_hints.flags & XCB_ICCCM_SIZE_HINT_US_SIZE || size_hints.flags & XCB_ICCCM_SIZE_HINT_P_SIZE)) { + DLOG("Setting geometry x=%d y=%d w=%d h=%d\n", size_hints.x, size_hints.y, size_hints.width, size_hints.height); + geom->x = size_hints.x; + geom->y = size_hints.y; + geom->width = size_hints.width; + geom->height = size_hints.height; + } + + /* If no aspect ratio was set or if it was invalid, we ignore the hints */ + if (size_hints.flags & XCB_ICCCM_SIZE_HINT_P_ASPECT && + (size_hints.min_aspect_num >= 0) && (size_hints.min_aspect_den > 0) && + (size_hints.max_aspect_num >= 0) && (size_hints.max_aspect_den > 0)) { + /* Convert numerator/denominator to a double */ + double min_aspect = (double)size_hints.min_aspect_num / size_hints.min_aspect_den; + double max_aspect = (double)size_hints.max_aspect_num / size_hints.max_aspect_den; + DLOG("Aspect ratio set: minimum %f, maximum %f\n", min_aspect, max_aspect); + if (fabs(win->min_aspect_ratio - min_aspect) > DBL_EPSILON) { + win->min_aspect_ratio = min_aspect; + changed = true; + } + if (fabs(win->max_aspect_ratio - max_aspect) > DBL_EPSILON) { + win->max_aspect_ratio = max_aspect; + changed = true; + } + } else { + DLOG("Clearing aspect ratios\n"); + + ASSIGN_IF_CHANGED(win->min_aspect_ratio, 0.0); + ASSIGN_IF_CHANGED(win->max_aspect_ratio, 0.0); + } + + return changed; +} + /* * Updates the WM_HINTS (we only care about the input focus handling part). * diff --git a/testcases/t/133-size-hints.t b/testcases/t/133-size-hints.t index a9a43cb5..a16c5399 100644 --- a/testcases/t/133-size-hints.t +++ b/testcases/t/133-size-hints.t @@ -16,31 +16,97 @@ # # Checks if size hints are interpreted correctly. # -use i3test; +use i3test i3_config => < [0, 0, 100, 100], + before_map => sub { + my ($window) = @_; + my $aspect = X11::XCB::Sizehints::Aspect->new; + $aspect->min_num($min_num); + $aspect->min_den($min_den); + $aspect->max_num($max_num); + $aspect->max_den($max_den); + $window->hints->aspect($aspect); + }); +} -my $win = open_window({ dont_map => 1 }); -# XXX: we should check screen size. in screens with an AR of 2.0, -# this is not a good idea. -my $aspect = X11::XCB::Sizehints::Aspect->new; -$aspect->min_num(600); -$aspect->min_den(300); -$aspect->max_num(600); -$aspect->max_den(300); -$win->_create; -$win->map; -wait_for_map $win; -$win->hints->aspect($aspect); -$x->flush; +################################################################################ +# Test aspect ratio set exactly to 2.0 +################################################################################ -sync_with_i3; +fresh_workspace; +my $win = open_with_aspect(600, 300, 600, 300); my $rect = $win->rect; my $ar = $rect->width / $rect->height; -diag("Aspect ratio = $ar"); -ok(($ar > 1.90) && ($ar < 2.10), 'Aspect ratio about 2.0'); +cmp_float($ar, 2, 'Window set to floating with aspect ratio 2.0'); + +cmd 'resize set 100'; +$rect = $win->rect; +$ar = $rect->width / $rect->height; +cmp_float($ar, 2, 'Window resized with aspect ratio kept to 2.0'); + +cmd 'resize set 400 100'; +$rect = $win->rect; +$ar = $rect->width / $rect->height; +cmp_float($ar, 2, 'Window resized with aspect ratio kept to 2.0'); + +# Also check that it is possible to resize by height only +cmd 'resize set height 400'; +$rect = $win->rect; +$ar = $rect->width / $rect->height; +is($rect->height, 400, 'Window height is 400px'); +cmp_float($ar, 2, 'Window resized with aspect ratio kept to 2.0'); + +cmd 'resize grow height 10'; +$rect = $win->rect; +$ar = $rect->width / $rect->height; +is($rect->height, 410, 'Window grew by 10px'); +cmp_float($ar, 2, 'Window resized with aspect ratio kept to 2.0'); + +################################################################################ +# Test aspect ratio between 0.5 and 2.0 +################################################################################ + +fresh_workspace; +$win = open_with_aspect(1, 2, 2, 1); + +$rect = $win->rect; +$ar = $rect->width / $rect->height; +cmp_float($ar, 1, 'Window set to floating with aspect ratio 1.0'); + +cmd 'resize set 200'; +$rect = $win->rect; +$ar = $rect->width / $rect->height; +is($rect->width, 200, 'Window width is 200px'); +is($rect->height, 100, 'Window height stayed 100px'); +cmp_float($ar, 2, 'Window resized, aspect ratio changed to 2.0'); + +cmd 'resize set 100 200'; +$rect = $win->rect; +$ar = $rect->width / $rect->height; +is($rect->width, 100, 'Window width is 100px'); +is($rect->height, 200, 'Window height is 200px'); +cmp_float($ar, 0.5, 'Window resized, aspect ratio changed to 0.5'); + +cmd 'resize set 500'; +$rect = $win->rect; +$ar = $rect->width / $rect->height; +cmp_float($ar, 2, 'Window resized, aspect ratio changed to maximum 2.0'); + +cmd 'resize set 100 400'; +$rect = $win->rect; +$ar = $rect->width / $rect->height; +cmp_float($ar, 0.5, 'Window resized, aspect ratio changed to minimum 0.5'); done_testing; From 100d05a2a662c339da616c6bdc3a1f7e3d95d355 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Tue, 2 Oct 2018 02:13:51 +0300 Subject: [PATCH 21/98] render_con: Get rid of render_fullscreen argument Only true for the fullscreen container and doesn't affect any of its children. Thus, we can get the same result by checking ->fullscreen_mode. --- include/render.h | 2 +- src/commands.c | 2 +- src/floating.c | 5 ++--- src/manage.c | 4 ++-- src/render.c | 24 ++++++++++++------------ src/tree.c | 2 +- 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/include/render.h b/include/render.h index 2b2c8dad..03751c01 100644 --- a/include/render.h +++ b/include/render.h @@ -40,7 +40,7 @@ typedef struct render_params { * updated in X11. * */ -void render_con(Con *con, bool render_fullscreen); +void render_con(Con *con); /** * Returns the height for the decorations diff --git a/src/commands.c b/src/commands.c index ee402846..ed5e482d 100644 --- a/src/commands.c +++ b/src/commands.c @@ -831,7 +831,7 @@ void cmd_append_layout(I3_CMD, const char *cpath) { // is not executed yet and will be batched with append_layout’s // needs_tree_render after the parser finished. We should check if that is // necessary at all. - render_con(croot, false); + render_con(croot); restore_open_placeholder_windows(parent); diff --git a/src/floating.c b/src/floating.c index 4f2760a7..c8b436b4 100644 --- a/src/floating.c +++ b/src/floating.c @@ -412,8 +412,7 @@ void floating_enable(Con *con, bool automatic) { DLOG("Corrected y = %d (deco_height = %d)\n", nc->rect.y, deco_height); /* render the cons to get initial window_rect correct */ - render_con(nc, false); - render_con(con, false); + render_con(nc); if (set_focus) con_activate(con); @@ -568,7 +567,7 @@ DRAGGING_CB(drag_window_callback) { con->rect.x = old_rect->x + (new_x - event->root_x); con->rect.y = old_rect->y + (new_y - event->root_y); - render_con(con, false); + render_con(con); x_push_node(con); xcb_flush(conn); diff --git a/src/manage.c b/src/manage.c index c222d351..63cadc0c 100644 --- a/src/manage.c +++ b/src/manage.c @@ -569,13 +569,13 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki * workspace at all. However, just calling render_con() on the * workspace isn’t enough either — it needs the rect. */ ws->rect = ws->parent->rect; - render_con(ws, true); + render_con(ws); /* Disable setting focus, otherwise we’d move focus to an invisible * workspace, which we generally prevent (e.g. in * con_move_to_workspace). */ set_focus = false; } - render_con(croot, false); + render_con(croot); /* Send an event about window creation */ ipc_send_window_event("new", nc); diff --git a/src/render.c b/src/render.c index 518d436b..e9e38ae0 100644 --- a/src/render.c +++ b/src/render.c @@ -37,16 +37,15 @@ int render_deco_height(void) { * updated in X11. * */ -void render_con(Con *con, bool render_fullscreen) { +void render_con(Con *con) { render_params params = { .rect = con->rect, .x = con->rect.x, .y = con->rect.y, .children = con_num_children(con)}; - DLOG("Rendering %snode %p / %s / layout %d / children %d\n", - (render_fullscreen ? "fullscreen " : ""), con, con->name, con->layout, - params.children); + DLOG("Rendering node %p / %s / layout %d / children %d\n", con, con->name, + con->layout, params.children); int i = 0; con->mapped = true; @@ -57,8 +56,9 @@ void render_con(Con *con, bool render_fullscreen) { * needs to be smaller */ Rect *inset = &(con->window_rect); *inset = (Rect){0, 0, con->rect.width, con->rect.height}; - if (!render_fullscreen) + if (con->fullscreen_mode == CF_NONE) { *inset = rect_add(*inset, con_border_style_rect(con)); + } /* Obey x11 border */ inset->width -= (2 * con->border_width); @@ -81,7 +81,7 @@ void render_con(Con *con, bool render_fullscreen) { if (fullscreen) { fullscreen->rect = params.rect; x_raise_con(fullscreen); - render_con(fullscreen, true); + render_con(fullscreen); /* Fullscreen containers are either global (underneath the CT_ROOT * container) or per-output (underneath the CT_CONTENT container). For * global fullscreen containers, we cannot abort rendering here yet, @@ -124,7 +124,7 @@ void render_con(Con *con, bool render_fullscreen) { DLOG("child at (%d, %d) with (%d x %d)\n", child->rect.x, child->rect.y, child->rect.width, child->rect.height); x_raise_con(child); - render_con(child, false); + render_con(child); i++; } @@ -137,7 +137,7 @@ void render_con(Con *con, bool render_fullscreen) { * that we have a non-leaf-container inside the stack. In that * case, the children of the non-leaf-container need to be raised * as well. */ - render_con(child, false); + render_con(child); } if (params.children != 1) @@ -186,7 +186,7 @@ static void render_root(Con *con, Con *fullscreen) { Con *output; if (!fullscreen) { TAILQ_FOREACH(output, &(con->nodes_head), nodes) { - render_con(output, false); + render_con(output); } } @@ -252,7 +252,7 @@ static void render_root(Con *con, Con *fullscreen) { DLOG("floating child at (%d,%d) with %d x %d\n", child->rect.x, child->rect.y, child->rect.width, child->rect.height); x_raise_con(child); - render_con(child, false); + render_con(child); } } } @@ -302,7 +302,7 @@ static void render_output(Con *con) { if (fullscreen) { fullscreen->rect = con->rect; x_raise_con(fullscreen); - render_con(fullscreen, true); + render_con(fullscreen); return; } @@ -342,7 +342,7 @@ static void render_output(Con *con) { DLOG("child at (%d, %d) with (%d x %d)\n", child->rect.x, child->rect.y, child->rect.width, child->rect.height); x_raise_con(child); - render_con(child, false); + render_con(child); } } diff --git a/src/tree.c b/src/tree.c index e3849873..99b03619 100644 --- a/src/tree.c +++ b/src/tree.c @@ -453,7 +453,7 @@ void tree_render(void) { mark_unmapped(croot); croot->mapped = true; - render_con(croot, false); + render_con(croot); x_push_changes(croot); DLOG("-- END RENDERING --\n"); From d2d6d6e0a86e479ab799e84a1ba15b0c13d288f0 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Tue, 2 Oct 2018 02:15:59 +0300 Subject: [PATCH 22/98] Re-render floating cons alone when possible --- src/floating.c | 6 ++---- src/handlers.c | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/floating.c b/src/floating.c index c8b436b4..bfb130fb 100644 --- a/src/floating.c +++ b/src/floating.c @@ -674,9 +674,7 @@ DRAGGING_CB(resize_window_callback) { con->rect.x = dest_x; con->rect.y = dest_y; - /* TODO: don’t re-render the whole tree just because we change - * coordinates of a floating window */ - tree_render(); + render_con(con); x_push_changes(croot); } @@ -957,7 +955,7 @@ bool floating_reposition(Con *con, Rect newrect) { /* Workspace change will already result in a tree_render. */ if (!reassigned) { - render_con(con, false); + render_con(con); x_push_node(con); } return true; diff --git a/src/handlers.c b/src/handlers.c index c4b8cd08..47abe2b4 100644 --- a/src/handlers.c +++ b/src/handlers.c @@ -980,7 +980,8 @@ static bool handle_normal_hints(void *data, xcb_connection_t *conn, uint8_t stat Con *floating = con_inside_floating(con); if (floating) { floating_check_size(con, false); - tree_render(); + render_con(con); + x_push_changes(croot); } } From 3463406df7aa1472efad9643e6799d0f5e9d66e8 Mon Sep 17 00:00:00 2001 From: Connor E <38229097+c-edw@users.noreply.github.com> Date: Tue, 13 Nov 2018 08:46:16 +0000 Subject: [PATCH 23/98] Update userguide docs for strip_workspace_*. --- docs/userguide | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/userguide b/docs/userguide index 635c7054..8a44e224 100644 --- a/docs/userguide +++ b/docs/userguide @@ -1628,14 +1628,16 @@ buttons. This is useful if you want to have a named workspace that stays in order on the bar according to its number without displaying the number prefix. When +strip_workspace_numbers+ is set to +yes+, any workspace that has a name of -the form "[n]:[NAME]" will display only the name. You could use this, for +the form "[n][:][NAME]" will display only the name. You could use this, for instance, to display Roman numerals rather than digits by naming your workspaces to "1:I", "2:II", "3:III", "4:IV", ... When +strip_workspace_name+ is set to +yes+, any workspace that has a name of -the form "[n]:[NAME]" will display only the number. +the form "[n][:][NAME]" will display only the number. -The default is to display the full name within the workspace button. +The default is to display the full name within the workspace button. Be aware +that the colon in the workspace name is optional, so `[n][NAME]` will also +have the the workspace name and number stripped correctly. *Syntax*: ------------------------------ From a84b30f8a995c38aa88b3333703595e052d650ab Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Thu, 15 Nov 2018 13:48:14 +0200 Subject: [PATCH 24/98] randr.c: Fix regression with focusing NULL container This was introduced in db3b9e41874400958cf85b461a5d1ff04a398fa3 which removed the NULL check for next. Fixes #3523. --- src/randr.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/randr.c b/src/randr.c index c0dec7f6..fb127ab5 100644 --- a/src/randr.c +++ b/src/randr.c @@ -974,10 +974,12 @@ void randr_disable_output(Output *output) { } } - /* Restore focus after con_detach / con_attach */ - DLOG("now focusing next = %p\n", next); - con_focus(next); - workspace_show(con_get_workspace(next)); + /* Restore focus after con_detach / con_attach. next can be NULL, see #3523. */ + if (next) { + DLOG("now focusing next = %p\n", next); + con_focus(next); + workspace_show(con_get_workspace(next)); + } /* 3: move the dock clients to the first output */ Con *child; From 7ade46c61f3d7ef4b1cdcffa1b6ad626da0ade42 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Wed, 28 Nov 2018 17:38:16 +0100 Subject: [PATCH 25/98] switch to clang-format-6.0 (#3533) --- src/floating.c | 2 +- travis/check-formatting.sh | 2 +- travis/travis-base-386.Dockerfile | 4 ++-- travis/travis-base-ubuntu-386.Dockerfile | 4 ++-- travis/travis-base-ubuntu.Dockerfile | 4 ++-- travis/travis-base.Dockerfile | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/floating.c b/src/floating.c index bfb130fb..f5c61782 100644 --- a/src/floating.c +++ b/src/floating.c @@ -892,7 +892,7 @@ drag_result_t drag_pointer(Con *con, const xcb_button_press_event_t *event, xcb_ XCB_CURRENT_TIME, XCB_GRAB_MODE_ASYNC, /* continue processing pointer events as normal */ XCB_GRAB_MODE_ASYNC /* keyboard mode */ - ); + ); if ((keyb_reply = xcb_grab_keyboard_reply(conn, keyb_cookie, &error)) == NULL) { ELOG("Could not grab keyboard (error_code = %d)\n", error->error_code); diff --git a/travis/check-formatting.sh b/travis/check-formatting.sh index d232339c..7aa2f565 100755 --- a/travis/check-formatting.sh +++ b/travis/check-formatting.sh @@ -3,4 +3,4 @@ set -e set -x -clang-format-4.0 -i $(find . -name "*.[ch]" | tr '\n' ' ') && git diff --exit-code || (echo 'Code was not formatted using clang-format!'; false) +clang-format-6.0 -i $(find . -name "*.[ch]" | tr '\n' ' ') && git diff --exit-code || (echo 'Code was not formatted using clang-format!'; false) diff --git a/travis/travis-base-386.Dockerfile b/travis/travis-base-386.Dockerfile index e6ad51f8..355c2588 100644 --- a/travis/travis-base-386.Dockerfile +++ b/travis/travis-base-386.Dockerfile @@ -13,12 +13,12 @@ RUN echo 'APT::Acquire::Retries "5";' > /etc/apt/apt.conf.d/80retry # (3608 kB/s)). Hence, let’s stick with httpredir.debian.org (default) for now. # Install mk-build-deps (for installing the i3 build dependencies), -# clang and clang-format-4.0 (for checking formatting and building with clang), +# clang and clang-format-6.0 (for checking formatting and building with clang), # lintian (for checking spelling errors), RUN linux32 apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ dpkg-dev devscripts git equivs \ - clang clang-format-4.0 \ + clang clang-format-6.0 \ lintian && \ rm -rf /var/lib/apt/lists/* diff --git a/travis/travis-base-ubuntu-386.Dockerfile b/travis/travis-base-ubuntu-386.Dockerfile index c71ad42d..d52df4b8 100644 --- a/travis/travis-base-ubuntu-386.Dockerfile +++ b/travis/travis-base-ubuntu-386.Dockerfile @@ -13,12 +13,12 @@ RUN echo 'APT::Acquire::Retries "5";' > /etc/apt/apt.conf.d/80retry # (3608 kB/s)). Hence, let’s stick with httpredir.debian.org (default) for now. # Install mk-build-deps (for installing the i3 build dependencies), -# clang and clang-format-4.0 (for checking formatting and building with clang), +# clang and clang-format-6.0 (for checking formatting and building with clang), # lintian (for checking spelling errors), RUN linux32 apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ dpkg-dev devscripts git equivs \ - clang clang-format-4.0 \ + clang clang-format-6.0 \ lintian && \ rm -rf /var/lib/apt/lists/* diff --git a/travis/travis-base-ubuntu.Dockerfile b/travis/travis-base-ubuntu.Dockerfile index 6b61e27e..d1057a39 100644 --- a/travis/travis-base-ubuntu.Dockerfile +++ b/travis/travis-base-ubuntu.Dockerfile @@ -13,13 +13,13 @@ RUN echo 'APT::Acquire::Retries "5";' > /etc/apt/apt.conf.d/80retry # (3608 kB/s)). Hence, let’s stick with httpredir.debian.org (default) for now. # Install mk-build-deps (for installing the i3 build dependencies), -# clang and clang-format-4.0 (for checking formatting and building with clang), +# clang and clang-format-6.0 (for checking formatting and building with clang), # lintian (for checking spelling errors), # test suite dependencies (for running tests) RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ dpkg-dev devscripts git equivs \ - clang clang-format-4.0 \ + clang clang-format-6.0 \ lintian && \ rm -rf /var/lib/apt/lists/* diff --git a/travis/travis-base.Dockerfile b/travis/travis-base.Dockerfile index 907002e8..def7598d 100644 --- a/travis/travis-base.Dockerfile +++ b/travis/travis-base.Dockerfile @@ -11,13 +11,13 @@ RUN echo 'APT::Acquire::Retries "5";' > /etc/apt/apt.conf.d/80retry # (3608 kB/s)). Hence, let’s stick with httpredir.debian.org (default) for now. # Install mk-build-deps (for installing the i3 build dependencies), -# clang and clang-format-4.0 (for checking formatting and building with clang), +# clang and clang-format-6.0 (for checking formatting and building with clang), # lintian (for checking spelling errors), # test suite dependencies (for running tests) RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ dpkg-dev devscripts git equivs \ - clang clang-format-4.0 \ + clang clang-format-6.0 \ lintian \ libmodule-install-perl libanyevent-perl libextutils-pkgconfig-perl xcb-proto cpanminus xvfb xserver-xephyr xauth libinline-perl libinline-c-perl libxml-simple-perl libmouse-perl libmousex-nativetraits-perl libextutils-depends-perl perl libtest-deep-perl libtest-exception-perl libxml-parser-perl libtest-simple-perl libtest-fatal-perl libdata-dump-perl libtest-differences-perl libxml-tokeparser-perl libipc-run-perl libxcb-xtest0-dev libx11-xcb-perl libjson-xs-perl x11-xserver-utils && \ rm -rf /var/lib/apt/lists/* From 9273f67734eeb8d303e63c3af6975482d3f58e80 Mon Sep 17 00:00:00 2001 From: Albert Safin Date: Fri, 9 Nov 2018 18:43:25 +0700 Subject: [PATCH 26/98] Log window id in state_for_frame() --- src/x.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/x.c b/src/x.c index c0c0961e..45601337 100644 --- a/src/x.c +++ b/src/x.c @@ -94,7 +94,7 @@ static con_state *state_for_frame(xcb_window_t window) { return state; /* TODO: better error handling? */ - ELOG("No state found\n"); + ELOG("No state found for window 0x%08x\n", window); assert(false); return NULL; } From 1a8561949882c359fcd0d53a81dde30c0bd0934e Mon Sep 17 00:00:00 2001 From: Albert Safin Date: Fri, 9 Nov 2018 06:19:08 +0700 Subject: [PATCH 27/98] Add input and bounding shapes support (#2742) Basic idea: if the window has a shape, set the parent container shape as the union of the window shape and the shape of the frame borders. Co-authored-by: Uli Schlachter --- configure.ac | 2 +- debian/control | 1 + include/data.h | 5 + include/handlers.h | 1 + include/i3.h | 3 +- include/x.h | 5 + src/handlers.c | 22 +++++ src/main.c | 21 ++++ src/manage.c | 17 ++++ src/tree.c | 5 + src/x.c | 209 +++++++++++++++++++++++++++++++++++----- testcases/t/301-shape.t | 114 ++++++++++++++++++++++ 12 files changed, 378 insertions(+), 27 deletions(-) create mode 100644 testcases/t/301-shape.t diff --git a/configure.ac b/configure.ac index 7ae01422..b961f61c 100644 --- a/configure.ac +++ b/configure.ac @@ -91,7 +91,7 @@ AX_PTHREAD dnl Each prefix corresponds to a source tarball which users might have dnl downloaded in a newer version and would like to overwrite. PKG_CHECK_MODULES([LIBSN], [libstartup-notification-1.0]) -PKG_CHECK_MODULES([XCB], [xcb xcb-xkb xcb-xinerama xcb-randr]) +PKG_CHECK_MODULES([XCB], [xcb xcb-xkb xcb-xinerama xcb-randr xcb-shape]) PKG_CHECK_MODULES([XCB_UTIL], [xcb-event xcb-util]) PKG_CHECK_MODULES([XCB_UTIL_CURSOR], [xcb-cursor]) PKG_CHECK_MODULES([XCB_UTIL_KEYSYMS], [xcb-keysyms]) diff --git a/debian/control b/debian/control index 71a2599c..843e6557 100644 --- a/debian/control +++ b/debian/control @@ -13,6 +13,7 @@ Build-Depends: debhelper (>= 9), libxcb-cursor-dev, libxcb-xrm-dev, libxcb-xkb-dev, + libxcb-shape0-dev, libxkbcommon-dev (>= 0.4.0), libxkbcommon-x11-dev (>= 0.4.0), asciidoc (>= 8.4.4), diff --git a/include/data.h b/include/data.h index d2b501b9..c3cada37 100644 --- a/include/data.h +++ b/include/data.h @@ -484,6 +484,11 @@ struct Window { /* aspect ratio from WM_NORMAL_HINTS (MPlayer uses this for example) */ double min_aspect_ratio; double max_aspect_ratio; + + /** The window has a nonrectangular shape. */ + bool shaped; + /** The window has a nonrectangular input shape. */ + bool input_shaped; }; /** diff --git a/include/handlers.h b/include/handlers.h index 1d5a3865..d2c79c59 100644 --- a/include/handlers.h +++ b/include/handlers.h @@ -16,6 +16,7 @@ extern int randr_base; extern int xkb_base; +extern int shape_base; /** * Adds the given sequence to the list of events which are ignored. diff --git a/include/i3.h b/include/i3.h index 93a7e0a3..e7afe7e5 100644 --- a/include/i3.h +++ b/include/i3.h @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -70,7 +71,7 @@ extern uint8_t root_depth; extern xcb_visualid_t visual_id; extern xcb_colormap_t colormap; -extern bool xcursor_supported, xkb_supported; +extern bool xcursor_supported, xkb_supported, shape_supported; extern xcb_window_t root; extern struct ev_loop *main_loop; extern bool only_check_config; diff --git a/include/x.h b/include/x.h index 8b7664f2..d01709ed 100644 --- a/include/x.h +++ b/include/x.h @@ -137,3 +137,8 @@ void x_set_warp_to(Rect *rect); * */ void x_mask_event_mask(uint32_t mask); + +/** + * Enables or disables nonrectangular shape of the container frame. + */ +void x_set_shape(Con *con, xcb_shape_sk_t kind, bool enable); diff --git a/src/handlers.c b/src/handlers.c index 47abe2b4..5a79ffe1 100644 --- a/src/handlers.c +++ b/src/handlers.c @@ -20,6 +20,7 @@ int randr_base = -1; int xkb_base = -1; int xkb_current_group; +int shape_base = -1; /* After mapping/unmapping windows, a notify event is generated. However, we don’t want it, since it’d trigger an infinite loop of switching between the different windows when @@ -1400,6 +1401,27 @@ void handle_event(int type, xcb_generic_event_t *event) { return; } + if (shape_supported && type == shape_base + XCB_SHAPE_NOTIFY) { + xcb_shape_notify_event_t *shape = (xcb_shape_notify_event_t *)event; + + DLOG("shape_notify_event for window 0x%08x, shape_kind = %d, shaped = %d\n", + shape->affected_window, shape->shape_kind, shape->shaped); + + Con *con = con_by_window_id(shape->affected_window); + if (con == NULL) { + LOG("Not a managed window 0x%08x, ignoring shape_notify_event\n", + shape->affected_window); + return; + } + + if (shape->shape_kind == XCB_SHAPE_SK_BOUNDING || + shape->shape_kind == XCB_SHAPE_SK_INPUT) { + x_set_shape(con, shape->shape_kind, shape->shaped); + } + + return; + } + switch (type) { case XCB_KEY_PRESS: case XCB_KEY_RELEASE: diff --git a/src/main.c b/src/main.c index 7eb47c82..b8b4bdf1 100644 --- a/src/main.c +++ b/src/main.c @@ -89,6 +89,7 @@ struct ws_assignments_head ws_assignments = TAILQ_HEAD_INITIALIZER(ws_assignment /* We hope that those are supported and set them to true */ bool xcursor_supported = true; bool xkb_supported = true; +bool shape_supported = true; bool force_xinerama = false; @@ -622,6 +623,9 @@ int main(int argc, char *argv[]) { xcb_set_root_cursor(XCURSOR_CURSOR_POINTER); const xcb_query_extension_reply_t *extreply; + xcb_prefetch_extension_data(conn, &xcb_xkb_id); + xcb_prefetch_extension_data(conn, &xcb_shape_id); + extreply = xcb_get_extension_data(conn, &xcb_xkb_id); xkb_supported = extreply->present; if (!extreply->present) { @@ -683,6 +687,23 @@ int main(int argc, char *argv[]) { xkb_base = extreply->first_event; } + /* Check for Shape extension. We want to handle input shapes which is + * introduced in 1.1. */ + extreply = xcb_get_extension_data(conn, &xcb_shape_id); + if (extreply->present) { + shape_base = extreply->first_event; + xcb_shape_query_version_cookie_t cookie = xcb_shape_query_version(conn); + xcb_shape_query_version_reply_t *version = + xcb_shape_query_version_reply(conn, cookie, NULL); + shape_supported = version && version->minor_version >= 1; + free(version); + } else { + shape_supported = false; + } + if (!shape_supported) { + DLOG("shape 1.1 is not present on this server\n"); + } + restore_connect(); property_handlers_init(); diff --git a/src/manage.c b/src/manage.c index 63cadc0c..c1468123 100644 --- a/src/manage.c +++ b/src/manage.c @@ -548,6 +548,23 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki * cleanup) */ xcb_change_save_set(conn, XCB_SET_MODE_INSERT, window); + if (shape_supported) { + /* Receive ShapeNotify events whenever the client altered its window + * shape. */ + xcb_shape_select_input(conn, window, true); + + /* Check if the window is shaped. Sadly, we can check only for the + * bounding shape, not for the input shape. */ + xcb_shape_query_extents_cookie_t cookie = + xcb_shape_query_extents(conn, window); + xcb_shape_query_extents_reply_t *reply = + xcb_shape_query_extents_reply(conn, cookie, NULL); + if (reply != NULL && reply->bounding_shaped) { + cwindow->shaped = true; + } + FREE(reply); + } + /* Check if any assignments match */ run_assignments(cwindow); diff --git a/src/tree.c b/src/tree.c index 99b03619..5023e894 100644 --- a/src/tree.c +++ b/src/tree.c @@ -248,6 +248,11 @@ bool tree_close_internal(Con *con, kill_window_t kill_window, bool dont_kill_par * mapped. See https://bugs.i3wm.org/1617 */ xcb_change_save_set(conn, XCB_SET_MODE_DELETE, con->window->id); + /* Stop receiving ShapeNotify events. */ + if (shape_supported) { + xcb_shape_select_input(conn, con->window->id, false); + } + /* Ignore X11 errors for the ReparentWindow request. * X11 Errors are returned when the window was already destroyed */ add_ignore_event(cookie.sequence, 0); diff --git a/src/x.c b/src/x.c index 45601337..a8e493dd 100644 --- a/src/x.c +++ b/src/x.c @@ -51,6 +51,11 @@ typedef struct con_state { bool need_reparent; xcb_window_t old_frame; + /* The container was child of floating container during the previous call of + * x_push_node(). This is used to remove the shape when the container is no + * longer floating. */ + bool was_floating; + Rect rect; Rect window_rect; @@ -396,6 +401,58 @@ static void x_draw_decoration_after_title(Con *con, struct deco_render_params *p x_draw_title_border(con, p); } +/* + * Get rectangles representing the border around the child window. Some borders + * are adjacent to the screen-edge and thus not returned. Return value is the + * number of rectangles. + * + */ +static size_t x_get_border_rectangles(Con *con, xcb_rectangle_t rectangles[4]) { + size_t count = 0; + int border_style = con_border_style(con); + + if (border_style != BS_NONE && con_is_leaf(con)) { + adjacent_t borders_to_hide = con_adjacent_borders(con) & config.hide_edge_borders; + Rect br = con_border_style_rect(con); + + if (!(borders_to_hide & ADJ_LEFT_SCREEN_EDGE)) { + rectangles[count++] = (xcb_rectangle_t){ + .x = 0, + .y = 0, + .width = br.x, + .height = con->rect.height, + }; + } + if (!(borders_to_hide & ADJ_RIGHT_SCREEN_EDGE)) { + rectangles[count++] = (xcb_rectangle_t){ + .x = con->rect.width + (br.width + br.x), + .y = 0, + .width = -(br.width + br.x), + .height = con->rect.height, + }; + } + if (!(borders_to_hide & ADJ_LOWER_SCREEN_EDGE)) { + rectangles[count++] = (xcb_rectangle_t){ + .x = br.x, + .y = con->rect.height + (br.height + br.y), + .width = con->rect.width + br.width, + .height = -(br.height + br.y), + }; + } + /* pixel border have an additional line at the top */ + if (border_style == BS_PIXEL && !(borders_to_hide & ADJ_UPPER_SCREEN_EDGE)) { + rectangles[count++] = (xcb_rectangle_t){ + .x = br.x, + .y = 0, + .width = con->rect.width + br.width, + .height = br.y, + }; + } + } + + return count; +} + /* * Draws the decoration of the given container onto its parent. * @@ -497,37 +554,24 @@ void x_draw_decoration(Con *con) { /* 3: draw a rectangle in border color around the client */ if (p->border_style != BS_NONE && p->con_is_leaf) { - /* We might hide some borders adjacent to the screen-edge */ - adjacent_t borders_to_hide = con_adjacent_borders(con) & config.hide_edge_borders; - Rect br = con_border_style_rect(con); - - /* These rectangles represent the border around the child window - * (left, bottom and right part). We don’t just fill the whole - * rectangle because some children are not freely resizable and we want - * their background color to "shine through". */ - if (!(borders_to_hide & ADJ_LEFT_SCREEN_EDGE)) { - draw_util_rectangle(&(con->frame_buffer), p->color->child_border, 0, 0, br.x, r->height); - } - if (!(borders_to_hide & ADJ_RIGHT_SCREEN_EDGE)) { - draw_util_rectangle(&(con->frame_buffer), - p->color->child_border, r->width + (br.width + br.x), 0, - -(br.width + br.x), r->height); - } - if (!(borders_to_hide & ADJ_LOWER_SCREEN_EDGE)) { - draw_util_rectangle(&(con->frame_buffer), - p->color->child_border, br.x, r->height + (br.height + br.y), - r->width + br.width, -(br.height + br.y)); - } - /* pixel border needs an additional line at the top */ - if (p->border_style == BS_PIXEL && !(borders_to_hide & ADJ_UPPER_SCREEN_EDGE)) { - draw_util_rectangle(&(con->frame_buffer), - p->color->child_border, br.x, 0, r->width + br.width, br.y); + /* Fill the border. We don’t just fill the whole rectangle because some + * children are not freely resizable and we want their background color + * to "shine through". */ + xcb_rectangle_t rectangles[4]; + size_t rectangles_count = x_get_border_rectangles(con, rectangles); + for (size_t i = 0; i < rectangles_count; i++) { + draw_util_rectangle(&(con->frame_buffer), p->color->child_border, + rectangles[i].x, + rectangles[i].y, + rectangles[i].width, + rectangles[i].height); } /* Highlight the side of the border at which the next window will be * opened if we are rendering a single window within a split container * (which is undistinguishable from a single window outside a split * container otherwise. */ + Rect br = con_border_style_rect(con); if (TAILQ_NEXT(con, nodes) == NULL && TAILQ_PREV(con, nodes_head, nodes) == NULL && con->parent->type != CT_FLOATING_CON) { @@ -730,6 +774,71 @@ static void set_hidden_state(Con *con) { state->is_hidden = should_be_hidden; } +/* + * Set the container frame shape as the union of the window shape and the + * shape of the frame borders. + */ +static void x_shape_frame(Con *con, xcb_shape_sk_t shape_kind) { + assert(con->window); + + xcb_shape_combine(conn, XCB_SHAPE_SO_SET, shape_kind, shape_kind, + con->frame.id, + con->window_rect.x + con->border_width, + con->window_rect.y + con->border_width, + con->window->id); + xcb_rectangle_t rectangles[4]; + size_t rectangles_count = x_get_border_rectangles(con, rectangles); + if (rectangles_count) { + xcb_shape_rectangles(conn, XCB_SHAPE_SO_UNION, shape_kind, + XCB_CLIP_ORDERING_UNSORTED, con->frame.id, + 0, 0, rectangles_count, rectangles); + } +} + +/* + * Reset the container frame shape. + */ +static void x_unshape_frame(Con *con, xcb_shape_sk_t shape_kind) { + assert(con->window); + + xcb_shape_mask(conn, XCB_SHAPE_SO_SET, shape_kind, con->frame.id, 0, 0, XCB_PIXMAP_NONE); +} + +/* + * Shape or unshape container frame based on the con state. + */ +static void set_shape_state(Con *con, bool need_reshape) { + if (!shape_supported || con->window == NULL) { + return; + } + + struct con_state *state; + if ((state = state_for_frame(con->frame.id)) == NULL) { + ELOG("window state for con %p not found\n", con); + return; + } + + if (need_reshape && con_is_floating(con)) { + /* We need to reshape the window frame only if it already has shape. */ + if (con->window->shaped) { + x_shape_frame(con, XCB_SHAPE_SK_BOUNDING); + } + if (con->window->input_shaped) { + x_shape_frame(con, XCB_SHAPE_SK_INPUT); + } + } + + if (state->was_floating && !con_is_floating(con)) { + /* Remove the shape when container is no longer floating. */ + if (con->window->shaped) { + x_unshape_frame(con, XCB_SHAPE_SK_BOUNDING); + } + if (con->window->input_shaped) { + x_unshape_frame(con, XCB_SHAPE_SK_INPUT); + } + } +} + /* * This function pushes the properties of each node of the layout tree to * X11 if they have changed (like the map state, position of the window, …). @@ -768,6 +877,8 @@ void x_push_node(Con *con) { con->mapped = false; } + bool need_reshape = false; + /* reparent the child window (when the window was moved due to a sticky * container) */ if (state->need_reparent && con->window != NULL) { @@ -793,8 +904,19 @@ void x_push_node(Con *con) { con->ignore_unmap++; DLOG("ignore_unmap for reparenting of con %p (win 0x%08x) is now %d\n", con, con->window->id, con->ignore_unmap); + + need_reshape = true; } + /* We need to update shape when window frame dimensions is updated. */ + need_reshape |= state->rect.width != rect.width || + state->rect.height != rect.height || + state->window_rect.width != con->window_rect.width || + state->window_rect.height != con->window_rect.height; + + /* We need to set shape when container becomes floating. */ + need_reshape |= con_is_floating(con) && !state->was_floating; + /* The pixmap of a borderless leaf container will not be used except * for the titlebar in a stack or tabs (issue #1013). */ bool is_pixmap_needed = (con->border_style != BS_NONE || @@ -898,6 +1020,8 @@ void x_push_node(Con *con) { fake_notify = true; } + set_shape_state(con, need_reshape); + /* Map if map state changed, also ensure that the child window * is changed if we are mapped and there is a new, unmapped child window. * Unmaps are handled in x_push_node_unmaps(). */ @@ -941,6 +1065,7 @@ void x_push_node(Con *con) { } state->unmap_now = (state->mapped != con->mapped) && !con->mapped; + state->was_floating = con_is_floating(con); if (fake_notify) { DLOG("Sending fake configure notify\n"); @@ -1325,3 +1450,37 @@ void x_mask_event_mask(uint32_t mask) { xcb_change_window_attributes(conn, state->id, XCB_CW_EVENT_MASK, values); } } + +/* + * Enables or disables nonrectangular shape of the container frame. + */ +void x_set_shape(Con *con, xcb_shape_sk_t kind, bool enable) { + struct con_state *state; + if ((state = state_for_frame(con->frame.id)) == NULL) { + ELOG("window state for con %p not found\n", con); + return; + } + + switch (kind) { + case XCB_SHAPE_SK_BOUNDING: + con->window->shaped = enable; + break; + case XCB_SHAPE_SK_INPUT: + con->window->input_shaped = enable; + break; + default: + ELOG("Received unknown shape event kind for con %p. This is a bug.\n", + con); + return; + } + + if (con_is_floating(con)) { + if (enable) { + x_shape_frame(con, kind); + } else { + x_unshape_frame(con, kind); + } + + xcb_flush(conn); + } +} diff --git a/testcases/t/301-shape.t b/testcases/t/301-shape.t new file mode 100644 index 00000000..ac0ec5a7 --- /dev/null +++ b/testcases/t/301-shape.t @@ -0,0 +1,114 @@ +#!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 shape support. +# Ticket: #2742 +use i3test; +use ExtUtils::PkgConfig; + +my %sn_config; +BEGIN { + %sn_config = ExtUtils::PkgConfig->find('xcb-shape'); +} + +use Inline C => Config => LIBS => $sn_config{libs}, CCFLAGS => $sn_config{cflags}; +use Inline C => <<'END_OF_C_CODE'; +#include + +static xcb_connection_t *conn; + +void init_ctx(void *connptr) { + conn = (xcb_connection_t*)connptr; +} + +/* + * Set the shape for the window consisting of the following zones: + * + * +---+---+ + * | A | B | + * +---+---+ + * | C | + * +-------+ + * + * - Zone A is completly opaque. + * - Zone B is clickable through (input shape). + * - Zone C is completly transparent (bounding shape). + */ +void set_shape(long window_id) { + xcb_rectangle_t bounding_rectangle = { 0, 0, 100, 50 }; + xcb_shape_rectangles(conn, XCB_SHAPE_SO_SET, XCB_SHAPE_SK_BOUNDING, + XCB_CLIP_ORDERING_UNSORTED, window_id, + 0, 0, 1, &bounding_rectangle); + xcb_rectangle_t input_rectangle = { 0, 0, 50, 50 }; + xcb_shape_rectangles(conn, XCB_SHAPE_SO_SET, XCB_SHAPE_SK_INPUT, + XCB_CLIP_ORDERING_UNSORTED, window_id, + 0, 0, 1, &input_rectangle); + xcb_flush(conn); +} +END_OF_C_CODE + +init_ctx($x->get_xcb_conn()); + +my ($ws, $win1, $win1_focus, $win2, $win2_focus); + +################################################################################ +# Case 1: make floating window, then set shape +################################################################################ + +$ws = fresh_workspace; + +$win1 = open_floating_window(rect => [0, 0, 100, 100], background_color => '#ff0000'); +$win1_focus = get_focused($ws); + +$win2 = open_floating_window(rect => [0, 0, 100, 100], background_color => '#00ff00'); +$win2_focus = get_focused($ws); +set_shape($win2->id); + +$win1->warp_pointer(75, 25); +sync_with_i3; +is(get_focused($ws), $win1_focus, 'focus switched to the underlying window'); + +$win1->warp_pointer(25, 25); +sync_with_i3; +is(get_focused($ws), $win2_focus, 'focus switched to the top window'); + +kill_all_windows; + +################################################################################ +# Case 2: set shape first, then make window floating +################################################################################ + +$ws = fresh_workspace; + +$win1 = open_window(rect => [0, 0, 100, 100], background_color => '#ff0000'); +$win1_focus = get_focused($ws); +cmd 'floating toggle'; + +$win2 = open_window(rect => [0, 0, 100, 100], background_color => '#00ff00'); +$win2_focus = get_focused($ws); +set_shape($win2->id); +cmd 'floating toggle'; +sync_with_i3; + +$win1->warp_pointer(75, 25); +sync_with_i3; +is(get_focused($ws), $win1_focus, 'focus switched to the underlying window'); + +$win1->warp_pointer(25, 25); +sync_with_i3; +is(get_focused($ws), $win2_focus, 'focus switched to the top window'); + +done_testing; From a81e22a277d81e7afc84d1bfe1cd8e00eaf683a7 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Tue, 4 Dec 2018 20:50:32 +0200 Subject: [PATCH 28/98] Apply title_align to non-leaf containers Additionally, marks will now display for non-leaf containers. Fixes #3540. --- src/x.c | 46 ++++++++++++++++++---------------------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/src/x.c b/src/x.c index a8e493dd..82c19d30 100644 --- a/src/x.c +++ b/src/x.c @@ -613,31 +613,6 @@ void x_draw_decoration(Con *con) { /* 6: draw the title */ int text_offset_y = (con->deco_rect.height - config.font.height) / 2; - struct Window *win = con->window; - if (win == NULL) { - i3String *title; - if (con->title_format == NULL) { - char *_title; - char *tree = con_get_tree_representation(con); - sasprintf(&_title, "i3: %s", tree); - free(tree); - - title = i3string_from_utf8(_title); - FREE(_title); - } else { - title = con_parse_title_format(con); - } - - draw_util_text(title, &(parent->frame_buffer), - p->color->text, p->color->background, - con->deco_rect.x + logical_px(2), - con->deco_rect.y + text_offset_y, - con->deco_rect.width - 2 * logical_px(2)); - I3STRING_FREE(title); - - goto after_title; - } - const int title_padding = logical_px(2); const int deco_width = (int)con->deco_rect.width; int mark_width = 0; @@ -677,7 +652,23 @@ void x_draw_decoration(Con *con) { FREE(formatted_mark); } - i3String *title = con->title_format == NULL ? win->name : con_parse_title_format(con); + i3String *title = NULL; + struct Window *win = con->window; + if (win == NULL) { + if (con->title_format == NULL) { + char *_title; + char *tree = con_get_tree_representation(con); + sasprintf(&_title, "i3: %s", tree); + free(tree); + + title = i3string_from_utf8(_title); + FREE(_title); + } else { + title = con_parse_title_format(con); + } + } else { + title = con->title_format == NULL ? win->name : con_parse_title_format(con); + } if (title == NULL) { goto copy_pixmaps; } @@ -710,11 +701,10 @@ void x_draw_decoration(Con *con) { con->deco_rect.y + text_offset_y, deco_width - mark_width - 2 * title_padding); - if (con->title_format != NULL) { + if (win == NULL || con->title_format != NULL) { I3STRING_FREE(title); } -after_title: x_draw_decoration_after_title(con, p); copy_pixmaps: draw_util_copy_surface(&(con->frame_buffer), &(con->frame), 0, 0, 0, 0, con->rect.width, con->rect.height); From 01c1b5dec2ae5a3ce54c9e0f41daf540afbf052f Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sat, 8 Dec 2018 13:31:53 +0100 Subject: [PATCH 29/98] Bugfix: use restore_conn, not conn Using the wrong X11 connection breaks the libev event handling model: xcb_flush() must be called immediately before handing control to libev. Before this fix: 1. xcb_prepare_cb would read and flush conn 2. restore_xcb_prepare_cb would read and flush restore_conn, BUT also inadvertantly call xcb_flush(conn), resulting in new events being filled into the XCB event queue 3. libev waits for new events 4. after 1 minute, libev times out and the events are processed Diagnosed using strace on testcases/complete-run.pl. related to commit 0d8b6714e39af81cbd6f4fbad500872a715dea24 related to #3510 --- src/restore_layout.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/restore_layout.c b/src/restore_layout.c index b99a50c1..9f19a690 100644 --- a/src/restore_layout.c +++ b/src/restore_layout.c @@ -180,8 +180,8 @@ static void update_placeholder_contents(placeholder_state *state) { int y = (state->rect.height / 2) - (config.font.height / 2); draw_util_text(line, &(state->surface), foreground, background, x, y, text_width); i3string_free(line); - xcb_flush(conn); - xcb_aux_sync(conn); + xcb_flush(restore_conn); + xcb_aux_sync(restore_conn); } static void open_placeholder_window(Con *con) { @@ -221,7 +221,7 @@ static void open_placeholder_window(Con *con) { state->con = con; state->rect = con->rect; - draw_util_surface_init(conn, &(state->surface), placeholder, get_visualtype(root_screen), state->rect.width, state->rect.height); + draw_util_surface_init(restore_conn, &(state->surface), placeholder, get_visualtype(root_screen), state->rect.width, state->rect.height); update_placeholder_contents(state); TAILQ_INSERT_TAIL(&state_head, state, state); From 9a1eb7a7834e449105acab7a7179ba206d6d17a7 Mon Sep 17 00:00:00 2001 From: Albert Safin Date: Sat, 8 Dec 2018 19:20:55 +0700 Subject: [PATCH 30/98] commands.c: Add missing error replies --- src/commands.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/commands.c b/src/commands.c index ed5e482d..9f408eb7 100644 --- a/src/commands.c +++ b/src/commands.c @@ -605,13 +605,17 @@ void cmd_resize(I3_CMD, const char *way, const char *direction, long resize_px, const double ppt = (double)resize_ppt / 100.0; if (!cmd_resize_tiling_width_height(current_match, cmd_output, current->con, direction, - resize_px, ppt)) + resize_px, ppt)) { + yerror("Cannot resize."); return; + } } else { if (!cmd_resize_tiling_direction(current_match, cmd_output, current->con, direction, - resize_px, resize_ppt)) + resize_px, resize_ppt)) { + yerror("Cannot resize."); return; + } } } } @@ -657,7 +661,7 @@ static bool resize_set_tiling(I3_CMD, Con *target, orientation_t resize_orientat void cmd_resize_set(I3_CMD, long cwidth, const char *mode_width, long cheight, const char *mode_height) { DLOG("resizing to %ld %s x %ld %s\n", cwidth, mode_width, cheight, mode_height); if (cwidth < 0 || cheight < 0) { - ELOG("Resize failed: dimensions cannot be negative (was %ld %s x %ld %s)\n", cwidth, mode_width, cheight, mode_height); + yerror("Dimensions cannot be negative."); return; } @@ -782,6 +786,7 @@ void cmd_append_layout(I3_CMD, const char *cpath) { char *buf = NULL; ssize_t len; if ((len = slurp(path, &buf)) < 0) { + yerror("Could not slurp \"%s\".", path); /* slurp already logged an error. */ goto out; } @@ -1513,7 +1518,7 @@ void cmd_layout(I3_CMD, const char *layout_str) { layout_t layout; if (!layout_from_name(layout_str, &layout)) { - ELOG("Unknown layout \"%s\", this is a mismatch between code and parser spec.\n", layout_str); + yerror("Unknown layout \"%s\", this is a mismatch between code and parser spec.", layout_str); return; } From 0eb07dea5c99eb7c2c5a9635bbdef75f0c8631fb Mon Sep 17 00:00:00 2001 From: Albert Safin Date: Thu, 6 Dec 2018 22:39:57 +0700 Subject: [PATCH 31/98] Remove unnecessary code in route_click() This case is handled by resize_find_tiling_participants() anyway which is introduced in the commit dbec5eb90585bc22752331f51d8a6bc90d21889c. --- src/click.c | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/click.c b/src/click.c index 1218a4c2..58ebbf3d 100644 --- a/src/click.c +++ b/src/click.c @@ -302,12 +302,6 @@ static int route_click(Con *con, xcb_button_press_event_t *event, const bool mod goto done; } - if (in_stacked) { - /* for stacked/tabbed cons, the resizing applies to the parent - * container */ - con = con->parent; - } - /* 7: floating modifier pressed, initiate a resize */ if (dest == CLICK_INSIDE && mod_pressed && event->detail == XCB_BUTTON_CLICK_RIGHT) { if (floating_mod_on_tiled_client(con, event)) From b6282b47bc757c3521eff090748bc81481913297 Mon Sep 17 00:00:00 2001 From: Albert Safin Date: Sun, 9 Dec 2018 06:15:53 +0700 Subject: [PATCH 32/98] Remove unused con_get_next() This function has unused for a long time since the commit 8f4b9ddaa43ae61932fd702ad7178c2b4e07cfb9. --- include/con.h | 7 ------- src/con.c | 36 ------------------------------------ 2 files changed, 43 deletions(-) diff --git a/include/con.h b/include/con.h index 88992076..09ec7b44 100644 --- a/include/con.h +++ b/include/con.h @@ -378,13 +378,6 @@ orientation_t con_orientation(Con *con); */ Con *con_next_focused(Con *con); -/** - * Get the next/previous container in the specified orientation. This may - * travel up until it finds a container with suitable orientation. - * - */ -Con *con_get_next(Con *con, char way, orientation_t orientation); - /** * Returns the focused con inside this client, descending the tree as far as * possible. This comes in handy when attaching a con to a workspace at the diff --git a/src/con.c b/src/con.c index 764419dc..f904bfe8 100644 --- a/src/con.c +++ b/src/con.c @@ -1485,42 +1485,6 @@ Con *con_next_focused(Con *con) { return next; } -/* - * Get the next/previous container in the specified orientation. This may - * travel up until it finds a container with suitable orientation. - * - */ -Con *con_get_next(Con *con, char way, orientation_t orientation) { - DLOG("con_get_next(way=%c, orientation=%d)\n", way, orientation); - /* 1: get the first parent with the same orientation */ - Con *cur = con; - while (con_orientation(cur->parent) != orientation) { - DLOG("need to go one level further up\n"); - if (cur->parent->type == CT_WORKSPACE) { - LOG("that's a workspace, we can't go further up\n"); - return NULL; - } - cur = cur->parent; - } - - /* 2: chose next (or previous) */ - Con *next; - if (way == 'n') { - next = TAILQ_NEXT(cur, nodes); - /* if we are at the end of the list, we need to wrap */ - if (next == TAILQ_END(&(parent->nodes_head))) - return NULL; - } else { - next = TAILQ_PREV(cur, nodes_head, nodes); - /* if we are at the end of the list, we need to wrap */ - if (next == TAILQ_END(&(cur->nodes_head))) - return NULL; - } - DLOG("next = %p\n", next); - - return next; -} - /* * Returns the focused con inside this client, descending the tree as far as * possible. This comes in handy when attaching a con to a workspace at the From 27030c8566f2e742c55fcabce9d718b5e884933b Mon Sep 17 00:00:00 2001 From: Albert Safin Date: Sun, 9 Dec 2018 07:06:29 +0700 Subject: [PATCH 33/98] Code style: fix misaligned and misindented comments --- src/bindings.c | 2 +- src/commands_parser.c | 2 +- src/config.c | 2 +- src/config_parser.c | 2 +- src/log.c | 6 +++--- src/main.c | 4 ++-- src/render.c | 14 +++++++------- src/scratchpad.c | 4 ++-- src/sd-daemon.c | 3 +-- src/window.c | 2 +- 10 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/bindings.c b/src/bindings.c index 6704c816..732543d0 100644 --- a/src/bindings.c +++ b/src/bindings.c @@ -123,7 +123,7 @@ static bool binding_in_current_group(const Binding *bind) { } static void grab_keycode_for_binding(xcb_connection_t *conn, Binding *bind, uint32_t keycode) { -/* Grab the key in all combinations */ + /* Grab the key in all combinations */ #define GRAB_KEY(modifier) \ do { \ xcb_grab_key(conn, 0, root, modifier, keycode, XCB_GRAB_MODE_SYNC, XCB_GRAB_MODE_ASYNC); \ diff --git a/src/commands_parser.c b/src/commands_parser.c index 4299c008..0da65adc 100644 --- a/src/commands_parser.c +++ b/src/commands_parser.c @@ -353,7 +353,7 @@ CommandResult *parse_command(const char *input, yajl_gen gen) { if (*walk == '\0' || *walk == ',' || *walk == ';') { next_state(token); token_handled = true; -/* To make sure we start with an appropriate matching + /* To make sure we start with an appropriate matching * datastructure for commands which do *not* specify any * criteria, we re-initialize the criteria system after * every command. */ diff --git a/src/config.c b/src/config.c index 9631b216..402771b1 100644 --- a/src/config.c +++ b/src/config.c @@ -202,7 +202,7 @@ void load_configuration(xcb_connection_t *conn, const char *override_configpath, /* Clear the old config or initialize the data structure */ memset(&config, 0, sizeof(config)); -/* Initialize default colors */ + /* Initialize default colors */ #define INIT_COLOR(x, cborder, cbackground, ctext, cindicator) \ do { \ x.border = draw_util_hex_to_color(cborder); \ diff --git a/src/config_parser.c b/src/config_parser.c index 9f972fed..c238f1b9 100644 --- a/src/config_parser.c +++ b/src/config_parser.c @@ -409,7 +409,7 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context) if (*walk == '\0' || *walk == '\n' || *walk == '\r') { next_state(token); token_handled = true; -/* To make sure we start with an appropriate matching + /* To make sure we start with an appropriate matching * datastructure for commands which do *not* specify any * criteria, we re-initialize the criteria system after * every command. */ diff --git a/src/log.c b/src/log.c index 916085f4..31db8b33 100644 --- a/src/log.c +++ b/src/log.c @@ -124,9 +124,9 @@ void init_logging(void) { */ void open_logbuffer(void) { /* Reserve 1% of the RAM for the logfile, but at max 25 MiB. - * For 512 MiB of RAM this will lead to a 5 MiB log buffer. - * At the moment (2011-12-10), no testcase leads to an i3 log - * of more than ~ 600 KiB. */ + * For 512 MiB of RAM this will lead to a 5 MiB log buffer. + * At the moment (2011-12-10), no testcase leads to an i3 log + * of more than ~ 600 KiB. */ logbuffer_size = min(physical_mem_bytes * 0.01, shmlog_size); #if defined(__FreeBSD__) sasprintf(&shmlogname, "/tmp/i3-log-%d", getpid()); diff --git a/src/main.c b/src/main.c index b8b4bdf1..4a20e0c0 100644 --- a/src/main.c +++ b/src/main.c @@ -529,7 +529,7 @@ int main(int argc, char *argv[]) { root_screen = xcb_aux_get_screen(conn, conn_screen); root = root_screen->root; -/* Place requests for the atoms we need as soon as possible */ + /* Place requests for the atoms we need as soon as possible */ #define xmacro(atom) \ xcb_intern_atom_cookie_t atom##_cookie = xcb_intern_atom(conn, 0, strlen(#atom), #atom); #include "atoms.xmacro" @@ -567,7 +567,7 @@ int main(int argc, char *argv[]) { xcb_get_geometry_cookie_t gcookie = xcb_get_geometry(conn, root); xcb_query_pointer_cookie_t pointercookie = xcb_query_pointer(conn, root); -/* Setup NetWM atoms */ + /* Setup NetWM atoms */ #define xmacro(name) \ do { \ xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply(conn, name##_cookie, NULL); \ diff --git a/src/render.c b/src/render.c index e9e38ae0..e16e36d7 100644 --- a/src/render.c +++ b/src/render.c @@ -134,17 +134,17 @@ void render_con(Con *con) { x_raise_con(child); if ((child = TAILQ_FIRST(&(con->focus_head)))) { /* By rendering the stacked container again, we handle the case - * that we have a non-leaf-container inside the stack. In that - * case, the children of the non-leaf-container need to be raised - * as well. */ + * that we have a non-leaf-container inside the stack. In that + * case, the children of the non-leaf-container need to be + * raised as well. */ render_con(child); } if (params.children != 1) - /* Raise the stack con itself. This will put the stack decoration on - * top of every stack window. That way, when a new window is opened in - * the stack, the old window will not obscure part of the decoration - * (it’s unmapped afterwards). */ + /* Raise the stack con itself. This will put the stack + * decoration on top of every stack window. That way, when a + * new window is opened in the stack, the old window will not + * obscure part of the decoration (it’s unmapped afterwards). */ x_raise_con(con); } } diff --git a/src/scratchpad.c b/src/scratchpad.c index a679f20a..b7fbcc92 100644 --- a/src/scratchpad.c +++ b/src/scratchpad.c @@ -121,8 +121,8 @@ bool scratchpad_show(Con *con) { DLOG("Found an unfocused scratchpad window on this workspace\n"); DLOG("Focusing it: %p\n", walk_con); /* use con_descend_tiling_focused to get the last focused - * window inside this scratch container in order to - * keep the focus the same within this container */ + * window inside this scratch container in order to + * keep the focus the same within this container */ con_activate(con_descend_tiling_focused(walk_con)); return true; } diff --git a/src/sd-daemon.c b/src/sd-daemon.c index dc33b2e9..28a88bfd 100644 --- a/src/sd-daemon.c +++ b/src/sd-daemon.c @@ -413,8 +413,7 @@ int sd_booted(void) { struct stat a, b; - /* We simply test whether the systemd cgroup hierarchy is - * mounted */ + /* We simply test whether the systemd cgroup hierarchy is mounted */ if (lstat("/sys/fs/cgroup", &a) < 0) return 0; diff --git a/src/window.c b/src/window.c index bec4c691..799488c6 100644 --- a/src/window.c +++ b/src/window.c @@ -439,7 +439,7 @@ void window_update_hints(i3Window *win, xcb_get_property_reply_t *prop, bool *ur * */ void window_update_motif_hints(i3Window *win, xcb_get_property_reply_t *prop, border_style_t *motif_border_style) { -/* This implementation simply mirrors Gnome's Metacity. Official + /* This implementation simply mirrors Gnome's Metacity. Official * documentation of this hint is nowhere to be found. * For more information see: * https://people.gnome.org/~tthurman/docs/metacity/xprops_8h-source.html From b479dc1a0f1b535f918445c4224373d8d172d7a2 Mon Sep 17 00:00:00 2001 From: TAL Date: Sun, 9 Dec 2018 01:32:34 +0100 Subject: [PATCH 34/98] Fix #3535 - Check for DISPLAY when requesting version information --- src/display_version.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/display_version.c b/src/display_version.c index 2e05cafa..da11bff3 100644 --- a/src/display_version.c +++ b/src/display_version.c @@ -55,6 +55,13 @@ static yajl_callbacks version_callbacks = { * */ void display_running_version(void) { + if (getenv("DISPLAY") == NULL) { + fprintf(stderr, "\nYour DISPLAY environment variable is not set.\n"); + fprintf(stderr, "Are you running i3 via SSH or on a virtual console?\n"); + fprintf(stderr, "Try DISPLAY=:0 i3 --moreversion\n"); + exit(EXIT_FAILURE); + } + char *pid_from_atom = root_atom_contents("I3_PID", conn, conn_screen); if (pid_from_atom == NULL) { /* If I3_PID is not set, the running version is older than 4.2-200. */ From d3e954befd5b8d8befc8b8cdfdfc398f9babce99 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Mon, 10 Dec 2018 18:18:23 +0100 Subject: [PATCH 35/98] userguide: add a section about hidpi displays This is a continuation of #3438. --- docs/userguide | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/userguide b/docs/userguide index 8a44e224..bf5a4d21 100644 --- a/docs/userguide +++ b/docs/userguide @@ -2842,3 +2842,20 @@ and you are in multi-monitor mode (see <>). Because i3 is not a compositing window manager, there is no ability to display a window on two screens at the same time. Instead, your presentation software needs to do this job (that is, open a window on each screen). + +[[hidpi]] +=== High-resolution displays (aka HIDPI displays) + +See https://wiki.archlinux.org/index.php/HiDPI for details on how to enable +scaling in various parts of the Linux desktop. i3 will read the desired DPI from +the `Xft.dpi` property. The property defaults to 96 DPI, so to achieve 200% +scaling, you’d set `Xft.dpi: 192` in `~/.Xresources`. + +If you are a long-time i3 user who just got a new monitor, double-check that: + +* You are using a scalable font (starting with “pango:”) in your i3 config. + +* You are using a terminal emulator which supports scaling. You could + temporarily switch to gnome-terminal, which is known to support scaling out of + the box, until you figure out how to adjust the font size in your favorite + terminal emulator. From 64ab1f42b7f5612c906fdda4c1e1e51cf690d7b2 Mon Sep 17 00:00:00 2001 From: Albert Safin Date: Mon, 10 Dec 2018 21:51:30 +0700 Subject: [PATCH 36/98] Preserve back_and_forth during restart Add new key "previous_workspace_name" to the json dump of the root container. --- include/workspace.h | 7 +++++++ src/ipc.c | 5 +++++ src/load_layout.c | 3 +++ src/workspace.c | 9 ++++++--- testcases/t/176-workspace-baf.t | 8 ++++++++ 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/include/workspace.h b/include/workspace.h index 28d9eb66..db544ce8 100644 --- a/include/workspace.h +++ b/include/workspace.h @@ -24,6 +24,13 @@ #define NET_WM_DESKTOP_NONE 0xFFFFFFF0 #define NET_WM_DESKTOP_ALL 0xFFFFFFFF +/** + * Stores a copy of the name of the last used workspace for the workspace + * back-and-forth switching. + * + */ +extern char *previous_workspace_name; + /** * Returns the workspace with the given name or NULL if such a workspace does * not exist. diff --git a/src/ipc.c b/src/ipc.c index d0fb965c..2ad5ae0f 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -651,6 +651,11 @@ void dump_node(yajl_gen gen, struct Con *con, bool inplace_restart) { y(integer, con->depth); } + if (inplace_restart && con->type == CT_ROOT && previous_workspace_name) { + ystr("previous_workspace_name"); + ystr(previous_workspace_name); + } + y(map_close); } diff --git a/src/load_layout.c b/src/load_layout.c index 003affee..47daada1 100644 --- a/src/load_layout.c +++ b/src/load_layout.c @@ -420,6 +420,9 @@ static int json_string(void *ctx, const unsigned char *val, size_t len) { else if (strcasecmp(buf, "changed") == 0) json_node->scratchpad_state = SCRATCHPAD_CHANGED; free(buf); + } else if (strcasecmp(last_key, "previous_workspace_name") == 0) { + FREE(previous_workspace_name); + previous_workspace_name = sstrndup((const char *)val, len); } } return 1; diff --git a/src/workspace.c b/src/workspace.c index f95073c9..2af88d73 100644 --- a/src/workspace.c +++ b/src/workspace.c @@ -11,9 +11,12 @@ #include "all.h" #include "yajl_utils.h" -/* Stores a copy of the name of the last used workspace for the workspace - * back-and-forth switching. */ -static char *previous_workspace_name = NULL; +/* + * Stores a copy of the name of the last used workspace for the workspace + * back-and-forth switching. + * + */ +char *previous_workspace_name = NULL; /* NULL-terminated list of workspace names (in order) extracted from * keybindings. */ diff --git a/testcases/t/176-workspace-baf.t b/testcases/t/176-workspace-baf.t index 01906e46..133e00fc 100644 --- a/testcases/t/176-workspace-baf.t +++ b/testcases/t/176-workspace-baf.t @@ -163,6 +163,14 @@ cmd 'scratchpad show'; cmd 'workspace back_and_forth'; is(focused_ws, '6: baz', 'workspace 6 now focused'); +################################################################################ +# See if BAF is preserved after restart +################################################################################ + +cmd 'restart'; +cmd 'workspace back_and_forth'; +is(focused_ws, '5: foo', 'workspace 5 focused after restart'); + exit_gracefully($pid); done_testing; From 90c08a52f00813403eb163e894c4f59288695295 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Tue, 6 Nov 2018 15:03:06 +0200 Subject: [PATCH 37/98] Introduce cmp_tree test function Related to #3503 --- testcases/lib/i3test.pm.in | 224 +++++++++++++++++++++++++++++++++++++ testcases/t/302-tree.t | 94 ++++++++++++++++ 2 files changed, 318 insertions(+) create mode 100644 testcases/t/302-tree.t diff --git a/testcases/lib/i3test.pm.in b/testcases/lib/i3test.pm.in index 5734eca7..740e13e9 100644 --- a/testcases/lib/i3test.pm.in +++ b/testcases/lib/i3test.pm.in @@ -53,6 +53,7 @@ our @EXPORT = qw( events_for listen_for_binding is_net_wm_state_focused + cmp_tree ); =head1 NAME @@ -1084,6 +1085,229 @@ sub is_net_wm_state_focused { return 0; } +=head2 cmp_tree([ $args ]) + +Compares the tree layout before and after an operation inside a subtest. + +The following arguments can be passed: + +=over 4 + +=item layout_before + +Required argument. The initial layout to be created. For example, +'H[ V[ a* S[ b c ] d ] e ]' or 'V[a b] T[c d*]'. +The layout will be converted to a JSON file which will be passed to i3's +append_layout command. + +The syntax's rules, assertions and limitations are: + +=over 8 + +=item 1. + +Upper case letters H, V, S, T mean horizontal, vertical, stacked and tabbed +layout respectively. They must be followed by an opening square bracket and must +be closed with a closing square bracket. +Each of the non-leaf containers is marked with their corresponding letter +followed by a number indicating the position of the container relative to other +containers of the same type. For example, 'H[V[xxx] V[xxx] H[xxx]]' will mark +the non-leaf containers as H1, V1, V2, H2. + +=item 2. + +Spaces are ignored. + +=item 3. + +Other alphanumeric characters mean a new window which uses the provided +character for its class and name. Eg 'H[a b]' will open windows with classes 'a' +and 'b' inside a horizontal split. Windows use a single character for their +class, eg 'H[xxx]' will open 3 windows with class 'x'. + +=item 4. + +Asterisks after a window mean that the window must be focused after the layout +is loaded. Currently, focusing non-leaf containers must be done manually, in the +callback (C) function. + +=back + +=item cb + +Subroutine to be called after the layout provided by C is created +but before the resulting layout (C) is checked. + +=item layout_after + +Required argument. The final layout in which the tree is expected to be after +the callback is called. Uses the same syntax with C. +For non-leaf containers, their layout (horizontal, vertical, stacked, tabbed) +is compared with the corresponding letter (H, V, S, T). +For leaf containers, their name is compared with the provided alphanumeric. + +=item ws + +The workspace in which the layout will be created. Will switch focus to it. If +not provided, a new one is created. + +=item msg + +Message to prepend to the subtest's name. If not empty, it will be followed by ': '. + +=item dont_kill + +By default, all windows are killed before the C layout is loaded. +Set to 1 to avoid this. + +=back + +=cut +sub cmp_tree { + local $Test::Builder::Level = $Test::Builder::Level + 1; + + my %args = @_; + my $ws = $args{ws}; + if (defined($ws)) { + cmd "workspace $ws"; + } else { + $ws = fresh_workspace; + } + my $msg = ''; + if ($args{msg}) { + $msg = $args{msg} . ': '; + } + die unless $args{layout_before}; + die unless $args{layout_after}; + + kill_all_windows unless $args{dont_kill}; + my @windows = create_layout($args{layout_before}); + Test::More::subtest $msg . $args{layout_before} . ' -> ' . $args{layout_after} => sub { + $args{cb}->(\@windows) if $args{cb}; + verify_layout($args{layout_after}, $ws); + }; + + return @windows; +} + +sub create_layout { + my $layout = shift; + + my $focus; + my @windows = (); + my $r = ''; + my $depth = 0; + my %layout_counts = (H => 0, V => 0, S => 0, T => 0); + + foreach my $char (split('', $layout)) { + if ($char eq 'H') { + $r = $r . '{"layout": "splith",'; + $r = $r . '"marks": ["H' . ++$layout_counts{H} . '"],'; + } elsif ($char eq 'V') { + $r = $r . '{"layout": "splitv",'; + $r = $r . '"marks": ["V' . ++$layout_counts{V} . '"],'; + } elsif ($char eq 'S') { + $r = $r . '{"layout": "stacked",'; + $r = $r . '"marks": ["S' . ++$layout_counts{S} . '"],'; + } elsif ($char eq 'T') { + $r = $r . '{"layout": "tabbed",'; + $r = $r . '"marks": ["T' . ++$layout_counts{T} . '"],'; + } elsif ($char eq '[') { + $depth++; + $r = $r . '"nodes": ['; + } elsif ($char eq ']') { + # End of nodes array: delete trailing comma. + chop $r; + # When we are at depth 0 we need to split using newlines, making + # multiple "JSON texts". + $depth--; + $r = $r . ']}' . ($depth == 0 ? "\n" : ','); + } elsif ($char eq ' ') { + } elsif ($char eq '*') { + $focus = $windows[$#windows]; + } elsif ($char =~ /[[:alnum:]]/) { + push @windows, $char; + + $r = $r . '{"swallows": [{'; + $r = $r . '"class": "^' . "$char" . '$"'; + $r = $r . '}]},'; + } else { + die "Could not understand $char"; + } + } + + die "Invalid layout, depth is $depth > 0" unless $depth == 0; + + Test::More::diag($r); + my ($fh, $tmpfile) = tempfile("layout-XXXXXX", UNLINK => 1); + print $fh "$r\n"; + close($fh); + + my $return = cmd "append_layout $tmpfile"; + die 'Could not parse layout json file' unless $return->[0]->{success}; + + my @result_windows; + push @result_windows, open_window(wm_class => "$_", name => "$_") foreach @windows; + cmd '[class=' . $focus . '] focus' if $focus; + + return @result_windows; +} + +sub verify_layout { + my ($layout, $ws) = @_; + + my $nodes = get_ws_content($ws); + my %counters; + my $depth = 0; + my $node; + + foreach my $char (split('', $layout)) { + my $node_name; + my $node_layout; + if ($char eq 'H') { + $node_layout = 'splith'; + } elsif ($char eq 'V') { + $node_layout = 'splitv'; + } elsif ($char eq 'S') { + $node_layout = 'stacked'; + } elsif ($char eq 'T') { + $node_layout = 'tabbed'; + } elsif ($char eq '[') { + $depth++; + delete $counters{$depth}; + } elsif ($char eq ']') { + $depth--; + } elsif ($char eq ' ') { + } elsif ($char eq '*') { + $tester->is_eq($node->{focused}, 1, 'Correct node focused'); + } elsif ($char =~ /[[:alnum:]]/) { + $node_name = $char; + } else { + die "Could not understand $char"; + } + + if ($node_layout || $node_name) { + if (exists($counters{$depth})) { + $counters{$depth} = $counters{$depth} + 1; + } else { + $counters{$depth} = 0; + } + + $node = $nodes->[$counters{0}]; + for my $i (1 .. $depth) { + $node = $node->{nodes}->[$counters{$i}]; + } + + if ($node_layout) { + $tester->is_eq($node->{layout}, $node_layout, "Layouts match in depth $depth, node number " . $counters{$depth}); + } else { + $tester->is_eq($node->{name}, $node_name, "Names match in depth $depth, node number " . $counters{$depth}); + } + } + } +} + + =head1 AUTHOR diff --git a/testcases/t/302-tree.t b/testcases/t/302-tree.t new file mode 100644 index 00000000..a2551a3e --- /dev/null +++ b/testcases/t/302-tree.t @@ -0,0 +1,94 @@ +#!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) +# +# Contains various tests that use the cmp_tree subroutine. +# Ticket: #3503 +use i3test; + +sub sanity_check { + local $Test::Builder::Level = $Test::Builder::Level + 1; + + my ($layout, $focus_idx) = @_; + my @windows = cmp_tree( + msg => 'Sanity check', + layout_before => $layout, + layout_after => $layout); + is($x->input_focus, $windows[$focus_idx]->id, 'Correct window focused') if $focus_idx >= 0; +} + +sanity_check('H[ V[ a* V[ b c ] d ] e ]', 0); +sanity_check('H[ a b c d* ]', 3); +sanity_check('V[a b] V[c d*]', 3); +sanity_check('T[a b] S[c*]', 2); + +cmp_tree( + msg => 'Simple focus test', + layout_before => 'H[a b] V[c* d]', + layout_after => 'H[a* b] V[c d]', + cb => sub { + cmd '[class=a] focus'; + }); + +cmp_tree( + msg => 'Simple move test', + layout_before => 'H[a b] V[c* d]', + layout_after => 'H[a b] V[d c*]', + cb => sub { + cmd 'move down'; + }); + +cmp_tree( + msg => 'Move from horizontal to vertical', + layout_before => 'H[a b] V[c d*]', + layout_after => 'H[b] V[c d a*]', + cb => sub { + cmd '[class=a] focus'; + cmd 'move right, move right'; + }); + +cmp_tree( + msg => 'Move unfocused non-leaf container', + layout_before => 'S[a b] V[c d* T[e f g]]', + layout_after => 'S[a T[e f g] b] V[c d*]', + cb => sub { + cmd '[con_mark=T1] move up, move up, move left, move up'; + }); + +cmp_tree( + msg => 'Simple swap test', + layout_before => 'H[a b] V[c d*]', + layout_after => 'H[a d*] V[c b]', + cb => sub { + cmd '[class=b] swap with id ' . $_[0][3]->{id}; + }); + +cmp_tree( + msg => 'Swap non-leaf containers', + layout_before => 'S[a b] V[c d*]', + layout_after => 'V[c d*] S[a b]', + cb => sub { + cmd '[con_mark=S1] swap with mark V1'; + }); + +cmp_tree( + msg => 'Swap nested non-leaf containers', + layout_before => 'S[a b] V[c d* T[e f g]]', + layout_after => 'T[e f g] V[c d* S[a b]]', + cb => sub { + cmd '[con_mark=S1] swap with mark T1'; + }); + +done_testing; From 605b6ba00f90ea5e0fb829101cff25f565e77c6b Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Wed, 12 Dec 2018 19:09:39 +0200 Subject: [PATCH 38/98] attach_to_workspace: set new parent before tree_render on_remove_child calls tree_close_internal which calls tree_render and the tree is in an invalid state if con->parent still points to the old parent. Fixes #3556 --- src/move.c | 7 +++--- testcases/t/303-regress-move-floating.t | 33 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 testcases/t/303-regress-move-floating.t diff --git a/src/move.c b/src/move.c index 545a910a..e28a91c6 100644 --- a/src/move.c +++ b/src/move.c @@ -178,9 +178,7 @@ void insert_con_into(Con *con, Con *target, position_t position) { */ static void attach_to_workspace(Con *con, Con *ws, direction_t direction) { con_detach(con); - con_fix_percent(con->parent); - CALL(con->parent, on_remove_child); - + Con *old_parent = con->parent; con->parent = ws; if (direction == D_RIGHT || direction == D_DOWN) { @@ -195,6 +193,9 @@ static void attach_to_workspace(Con *con, Con *ws, direction_t direction) { * does not make sense anyways. */ con->percent = 0.0; con_fix_percent(ws); + + con_fix_percent(old_parent); + CALL(old_parent, on_remove_child); } /* diff --git a/testcases/t/303-regress-move-floating.t b/testcases/t/303-regress-move-floating.t new file mode 100644 index 00000000..396e586f --- /dev/null +++ b/testcases/t/303-regress-move-floating.t @@ -0,0 +1,33 @@ +#!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) +# +# Regression: moving a container which is the only child of the only child of a +# floating container crashes i3. +# Ticket: #3556 +# Bug still in: 4.16-61-g376833db4 +use i3test; + +my $ws = fresh_workspace; +open_window; +open_window; +cmd 'split v, focus parent, floating toggle, focus child, move right'; +does_i3_live; + +$ws = get_ws($ws); +is(scalar @{$ws->{floating_nodes}}, 0, 'No floating nodes in workspace'); +is(scalar @{$ws->{nodes}}, 2, 'Two tiling nodes in workspace'); + +done_testing; From fdda9763b725f753909010e997e90f710a437ab4 Mon Sep 17 00:00:00 2001 From: Vladimir Panteleev Date: Fri, 14 Dec 2018 08:29:23 +0000 Subject: [PATCH 39/98] userguide: Document mark --replace flag - Explicitly document --replace, which was previously only mentioned in the command syntax. - Improve wording: "a window can only have one mark" is slightly misleading because it appears to describe the limitation as a property of the model, whereas this actually pertains the mark command. --- docs/userguide | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/userguide b/docs/userguide index bf5a4d21..55e80ce1 100644 --- a/docs/userguide +++ b/docs/userguide @@ -2416,8 +2416,9 @@ 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. +The +--replace+ flag causes i3 to remove any existing marks, which is also the +default behavior. You can use the +--add+ flag to put more than one mark on a +window. Refer to <> if you don't want marks to be shown in the window decoration. From 7d89e90137c17e1745fdc3886d6c3d64117201a9 Mon Sep 17 00:00:00 2001 From: Vladimir Panteleev Date: Fri, 14 Dec 2018 13:16:11 +0000 Subject: [PATCH 40/98] userguide: Un-hide a TODO block completed in 2011 The userguide contained a commented-out section for marks, which included the line: > TODO: make i3-input replace %s The line was added in a26a11c6099f82fc9fd8b87f8bda128408b4f7c9 (May 2011), at which point i3-input did not have a -F switch. The switch was added only in 1737a78fcd8025e11398dbcf5acd65c6a07ae86d (September 2011), but the documentation was never updated to enable the commented-out examples. --- docs/userguide | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/userguide b/docs/userguide index bf5a4d21..073ffdf1 100644 --- a/docs/userguide +++ b/docs/userguide @@ -2428,6 +2428,21 @@ mark [--add|--replace] [--toggle] unmark ---------------------------------------------- +You can use +i3-input+ to prompt for a mark name, then use the +mark+ +and +focus+ commands to create and jump to custom marks: + +*Examples*: +--------------------------------------- +# read 1 character and mark the current window with this character +bindsym $mod+m exec i3-input -F 'mark %s' -l 1 -P 'Mark: ' + +# read 1 character and go to the window with the character +bindsym $mod+g exec i3-input -F '[con_mark="%s"] focus' -l 1 -P 'Goto: ' +--------------------------------------- + +Alternatively, if you do not want to mess with +i3-input+, you could create +separate bindings for a specific set of labels and then only use those labels: + *Example (in a terminal)*: --------------------------------------------------------- # marks the focused container @@ -2443,21 +2458,6 @@ unmark irssi [class="(?i)firefox"] unmark --------------------------------------------------------- -/////////////////////////////////////////////////////////////////// -TODO: make i3-input replace %s -*Examples*: ---------------------------------------- -# Read 1 character and mark the current window with this character -bindsym $mod+m exec i3-input -F 'mark %s' -l 1 -P 'Mark: ' - -# Read 1 character and go to the window with the character -bindsym $mod+g exec i3-input -F '[con_mark="%s"] focus' -l 1 -P 'Goto: ' ---------------------------------------- - -Alternatively, if you do not want to mess with +i3-input+, you could create -separate bindings for a specific set of labels and then only use those labels. -/////////////////////////////////////////////////////////////////// - [[pango_markup]] === Window title format From f70c3b168d6af594fc02fd257bf9dd810bdca894 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Fri, 14 Dec 2018 23:35:58 +0200 Subject: [PATCH 41/98] Update tree_close_internal documentation in tree.h After f90840337 --- include/tree.h | 4 ---- 1 file changed, 4 deletions(-) diff --git a/include/tree.h b/include/tree.h index 41a63036..12170f94 100644 --- a/include/tree.h +++ b/include/tree.h @@ -73,10 +73,6 @@ void tree_next(char way, orientation_t orientation); * The dont_kill_parent flag is specified when the function calls itself * recursively while deleting a containers children. * - * The force_set_focus flag is specified in the case of killing a floating - * window: tree_close_internal() will be invoked for the CT_FLOATINGCON (the parent - * container) and focus should be set there. - * */ bool tree_close_internal(Con *con, kill_window_t kill_window, bool dont_kill_parent); From dd708199ea2782b6ee05a7fb0ec041424a7c3a7b Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Fri, 14 Dec 2018 20:37:29 +0200 Subject: [PATCH 42/98] Fix: killing unfocused window shouldn't produce focus event --- src/x.c | 7 ++++++- testcases/t/219-ipc-window-focus.t | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/x.c b/src/x.c index 82c19d30..a2f2c39f 100644 --- a/src/x.c +++ b/src/x.c @@ -276,7 +276,12 @@ static void _x_con_kill(Con *con) { free(state); /* Invalidate focused_id to correctly focus new windows with the same ID */ - focused_id = last_focused = XCB_NONE; + if (con->frame.id == focused_id) { + focused_id = XCB_NONE; + } + if (con->frame.id == last_focused) { + last_focused = XCB_NONE; + } } /* diff --git a/testcases/t/219-ipc-window-focus.t b/testcases/t/219-ipc-window-focus.t index b1c8ba18..afb1bfbc 100644 --- a/testcases/t/219-ipc-window-focus.t +++ b/testcases/t/219-ipc-window-focus.t @@ -44,11 +44,26 @@ sub focus_subtest { is($events[0]->{container}->{name}, $name, "$name focused"); } +sub kill_subtest { + my ($cmd, $name) = @_; + + my $focus = AnyEvent->condvar; + + my @events = events_for( + sub { cmd $cmd }, + 'window'); + + is(scalar @events, 1, 'Received 1 event'); + is($events[0]->{change}, 'close', 'Close event received'); + is($events[0]->{container}->{name}, $name, "$name closed"); +} + subtest 'focus left (1)', \&focus_subtest, 'focus left', $win1->name; subtest 'focus left (2)', \&focus_subtest, 'focus left', $win0->name; subtest 'focus right (1)', \&focus_subtest, 'focus right', $win1->name; subtest 'focus right (2)', \&focus_subtest, 'focus right', $win2->name; subtest 'focus right (3)', \&focus_subtest, 'focus right', $win0->name; subtest 'focus left', \&focus_subtest, 'focus left', $win2->name; +subtest 'kill doesn\'t produce focus event', \&kill_subtest, '[id=' . $win1->id . '] kill', $win1->name; done_testing; From fb1ae61d1c25e0f61c23cac82f2af511c336c6bf Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Fri, 14 Dec 2018 20:39:34 +0200 Subject: [PATCH 43/98] Invalidate last_focused when focusing the EWMH support window Fixes #3562 --- src/x.c | 1 + testcases/t/219-ipc-window-focus.t | 38 +++++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/x.c b/src/x.c index a2f2c39f..f643a9b3 100644 --- a/src/x.c +++ b/src/x.c @@ -1325,6 +1325,7 @@ void x_push_changes(Con *con) { change_ewmh_focus(XCB_WINDOW_NONE, last_focused); focused_id = ewmh_window; + last_focused = XCB_NONE; } xcb_flush(conn); diff --git a/testcases/t/219-ipc-window-focus.t b/testcases/t/219-ipc-window-focus.t index afb1bfbc..696fc7b2 100644 --- a/testcases/t/219-ipc-window-focus.t +++ b/testcases/t/219-ipc-window-focus.t @@ -14,14 +14,20 @@ # • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf # (unless you are already familiar with Perl) -use i3test; +use i3test i3_config => < 0); my $win0 = open_window; my $win1 = open_window; my $win2 = open_window; @@ -66,4 +72,30 @@ subtest 'focus right (3)', \&focus_subtest, 'focus right', $win0->name; subtest 'focus left', \&focus_subtest, 'focus left', $win2->name; subtest 'kill doesn\'t produce focus event', \&kill_subtest, '[id=' . $win1->id . '] kill', $win1->name; +# See issue #3562. We need to switch to an existing workspace on the second +# output to trigger the bug. +cmd 'workspace X'; +subtest 'workspace focus', \&focus_subtest, "workspace $ws", $win2->name; + +sub scratchpad_subtest { + my ($cmd, $name) = @_; + + my $focus = AnyEvent->condvar; + + my @events = events_for( + sub { cmd $cmd }, + 'window'); + + is(scalar @events, 2, 'Received 2 events'); + is($events[0]->{change}, 'move', 'Move event received'); + is($events[0]->{container}->{nodes}->[0]->{name}, $name, "$name moved"); + is($events[1]->{change}, 'focus', 'Focus event received'); + is($events[1]->{container}->{name}, $name, "$name focused"); +} + +fresh_workspace; +my $win = open_window; +cmd 'move scratchpad'; +subtest 'scratchpad', \&scratchpad_subtest, '[id=' . $win->id . '] scratchpad show', $win->name; + done_testing; From 318528e3fcc472e8ee1d39cda56bbe86d5b3f720 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Sun, 16 Dec 2018 03:27:09 +0200 Subject: [PATCH 44/98] Fix: render_con shows floating containers on wrong workspace After 204eefc. Alternative fix: diff --git a/src/floating.c b/src/floating.c index f5c61782..6dd79668 100644 --- a/src/floating.c +++ b/src/floating.c @@ -954,7 +954,7 @@ bool floating_reposition(Con *con, Rect newrect) { con->scratchpad_state = SCRATCHPAD_CHANGED; /* Workspace change will already result in a tree_render. */ - if (!reassigned) { + if (!reassigned && workspace_is_visible(con_get_workspace(con))) { render_con(con); x_push_node(con); } but I don't think that the extra complexity is worth it. Change in handlers.c because of d2d6d6e0 where the bug also appears. Fixes #3567 --- src/floating.c | 3 +-- src/handlers.c | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/floating.c b/src/floating.c index f5c61782..79f1d3d3 100644 --- a/src/floating.c +++ b/src/floating.c @@ -955,8 +955,7 @@ bool floating_reposition(Con *con, Rect newrect) { /* Workspace change will already result in a tree_render. */ if (!reassigned) { - render_con(con); - x_push_node(con); + tree_render(); } return true; } diff --git a/src/handlers.c b/src/handlers.c index 5a79ffe1..ae42b82e 100644 --- a/src/handlers.c +++ b/src/handlers.c @@ -981,8 +981,7 @@ static bool handle_normal_hints(void *data, xcb_connection_t *conn, uint8_t stat Con *floating = con_inside_floating(con); if (floating) { floating_check_size(con, false); - render_con(con); - x_push_changes(croot); + tree_render(); } } From 6462cf1ca3f25b5d2cbb89604de00e19465bd984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=C3=A1i=20Ng=E1=BB=8Dc=20Duy?= Date: Wed, 2 Jan 2019 19:05:18 +0700 Subject: [PATCH 45/98] Remove \n from errx and die messages errx() already appends \n internally. "\n" in the error message will result in a blank line after the message. die() is just a wrapper around errx() so it receives the same treatment. --- i3-config-wizard/main.c | 6 +++--- i3-input/main.c | 2 +- i3-nagbar/main.c | 4 ++-- src/main.c | 2 +- src/restore_layout.c | 2 +- testcases/inject_randr1.5.c | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/i3-config-wizard/main.c b/i3-config-wizard/main.c index 4b556657..7c1f00a3 100644 --- a/i3-config-wizard/main.c +++ b/i3-config-wizard/main.c @@ -822,7 +822,7 @@ int main(int argc, char *argv[]) { int screen; if ((conn = xcb_connect(NULL, &screen)) == NULL || xcb_connection_has_error(conn)) - errx(1, "Cannot open display\n"); + errx(1, "Cannot open display"); if (xkb_x11_setup_xkb_extension(conn, XKB_X11_MIN_MAJOR_XKB_VERSION, @@ -859,7 +859,7 @@ int main(int argc, char *argv[]) { root = root_screen->root; if (!(modmap_reply = xcb_get_modifier_mapping_reply(conn, modmap_cookie, NULL))) - errx(EXIT_FAILURE, "Could not get modifier mapping\n"); + errx(EXIT_FAILURE, "Could not get modifier mapping"); xcb_numlock_mask = get_mod_mask_for(XCB_NUM_LOCK, symbols, modmap_reply); @@ -899,7 +899,7 @@ int main(int argc, char *argv[]) { do { \ xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply(conn, name##_cookie, NULL); \ if (!reply) \ - errx(EXIT_FAILURE, "Could not get atom " #name "\n"); \ + errx(EXIT_FAILURE, "Could not get atom " #name); \ \ A_##name = reply->atom; \ free(reply); \ diff --git a/i3-input/main.c b/i3-input/main.c index d1a2efd7..f15a74ab 100644 --- a/i3-input/main.c +++ b/i3-input/main.c @@ -435,7 +435,7 @@ int main(int argc, char *argv[]) { int screen; conn = xcb_connect(NULL, &screen); if (!conn || xcb_connection_has_error(conn)) - die("Cannot open display\n"); + die("Cannot open display"); sockfd = ipc_connect(socket_path); diff --git a/i3-nagbar/main.c b/i3-nagbar/main.c index 4ce74939..ec3e25fb 100644 --- a/i3-nagbar/main.c +++ b/i3-nagbar/main.c @@ -418,7 +418,7 @@ int main(int argc, char *argv[]) { int screens; if ((conn = xcb_connect(NULL, &screens)) == NULL || xcb_connection_has_error(conn)) - die("Cannot open display\n"); + die("Cannot open display"); /* Place requests for the atoms we need as soon as possible */ #define xmacro(atom) \ @@ -512,7 +512,7 @@ int main(int argc, char *argv[]) { do { \ xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply(conn, name##_cookie, NULL); \ if (!reply) \ - die("Could not get atom " #name "\n"); \ + die("Could not get atom " #name); \ \ A_##name = reply->atom; \ free(reply); \ diff --git a/src/main.c b/src/main.c index 4a20e0c0..074afe71 100644 --- a/src/main.c +++ b/src/main.c @@ -515,7 +515,7 @@ int main(int argc, char *argv[]) { conn = xcb_connect(NULL, &conn_screen); if (xcb_connection_has_error(conn)) - errx(EXIT_FAILURE, "Cannot open display\n"); + errx(EXIT_FAILURE, "Cannot open display"); sndisplay = sn_xcb_display_new(conn, NULL, NULL); diff --git a/src/restore_layout.c b/src/restore_layout.c index 9f19a690..186c0907 100644 --- a/src/restore_layout.c +++ b/src/restore_layout.c @@ -115,7 +115,7 @@ void restore_connect(void) { #ifdef I3_ASAN_ENABLED __lsan_do_leak_check(); #endif - errx(EXIT_FAILURE, "Cannot open display\n"); + errx(EXIT_FAILURE, "Cannot open display"); } xcb_watcher = scalloc(1, sizeof(struct ev_io)); diff --git a/testcases/inject_randr1.5.c b/testcases/inject_randr1.5.c index 520b0213..29b6a33d 100644 --- a/testcases/inject_randr1.5.c +++ b/testcases/inject_randr1.5.c @@ -402,7 +402,7 @@ int main(int argc, char *argv[]) { } if (optind >= argc) { - errx(EXIT_FAILURE, "syntax: %s [options] \n", argv[0]); + errx(EXIT_FAILURE, "syntax: %s [options] ", argv[0]); } int fd = socket(AF_LOCAL, SOCK_STREAM, 0); From 0abb4f7e88542bcdd47094450eeec22bdbf266d6 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Fri, 11 Jan 2019 14:44:39 +0200 Subject: [PATCH 46/98] Fix crash with popups when fullscreen is non-leaf Introduced in b3e69ed12 Fixes #3582 --- src/render.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/render.c b/src/render.c index e16e36d7..42992f3a 100644 --- a/src/render.c +++ b/src/render.c @@ -214,7 +214,7 @@ static void render_root(Con *con, Con *fullscreen) { * fullscreen work correctly (ticket #564). Exception to the * above rule: smart popup_during_fullscreen handling (popups * belonging to the fullscreen app will be rendered). */ - if (config.popup_during_fullscreen != PDF_SMART) { + if (config.popup_during_fullscreen != PDF_SMART || fullscreen->window == NULL) { continue; } From 5aa0459be0a8471226dabda620c028c149966b29 Mon Sep 17 00:00:00 2001 From: Orestis Date: Sat, 12 Jan 2019 14:13:03 +0200 Subject: [PATCH 47/98] Use ipc queue for all messages (#3585) I was able to reproduce #3579 in Linux by running: `sudo sysctl net.core.wmem_default=10000` If a subscription message was too big to be sent at once, it was possible to break a client by sending a reply to an other message sent by the client. Eg: - Write 8192 out of 11612 bytes of a workspace event. - Blockingly write the reply to a workspace change message. - Write the rest 3420 bytes of the workspace event. This commit fixes this by utilizing the ipc queue for all types of writes. ipc_receive_message can only be called from a callback started in ipc_new_client. This callback uses the same file descriptor with the client also created in ipc_new_client. When the client is deleted, the read callback is now also stopped. Thus, we can assume that whenever ipc_receive_message is called, the corresponding client should still exist. - ipc_client now contains pointers to both write and read watchers. When freed, a client will stop both of them. - IPC_HANDLERs now work with ipc_clients instead of fds. Fixes #3579. --- include/ipc.h | 11 +-- src/ipc.c | 221 +++++++++++++++++++++----------------------------- 2 files changed, 99 insertions(+), 133 deletions(-) diff --git a/include/ipc.h b/include/ipc.h index a1caea82..a7743973 100644 --- a/include/ipc.h +++ b/include/ipc.h @@ -35,7 +35,8 @@ typedef struct ipc_client { * event has been sent by i3. */ bool first_tick_sent; - struct ev_io *callback; + struct ev_io *read_callback; + struct ev_io *write_callback; struct ev_timer *timeout; uint8_t *buffer; size_t buffer_size; @@ -54,12 +55,12 @@ typedef struct ipc_client { * message_type is the type of the message as the sender specified it. * */ -typedef void (*handler_t)(int, uint8_t *, int, uint32_t, uint32_t); +typedef void (*handler_t)(ipc_client *, uint8_t *, int, uint32_t, uint32_t); /* Macro to declare a callback */ -#define IPC_HANDLER(name) \ - static void handle_##name(int fd, uint8_t *message, \ - int size, uint32_t message_size, \ +#define IPC_HANDLER(name) \ + static void handle_##name(ipc_client *client, uint8_t *message, \ + int size, uint32_t message_size, \ uint32_t message_type) /** diff --git a/src/ipc.c b/src/ipc.c index 2ad5ae0f..2432d7a5 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -38,46 +38,6 @@ static void set_nonblock(int sockfd) { err(-1, "Could not set O_NONBLOCK"); } -/* - * Given a message and a message type, create the corresponding header, merge it - * with the message and append it to the given client's output buffer. - * - */ -static void append_payload(ipc_client *client, uint32_t message_type, const char *payload) { - const size_t size = strlen(payload); - const i3_ipc_header_t header = { - .magic = {'i', '3', '-', 'i', 'p', 'c'}, - .size = size, - .type = message_type}; - const size_t header_size = sizeof(i3_ipc_header_t); - const size_t message_size = header_size + size; - - client->buffer = srealloc(client->buffer, client->buffer_size + message_size); - memcpy(client->buffer + client->buffer_size, ((void *)&header), header_size); - memcpy(client->buffer + client->buffer_size + header_size, payload, size); - client->buffer_size += message_size; -} - -static void free_ipc_client(ipc_client *client) { - close(client->fd); - - ev_io_stop(main_loop, client->callback); - FREE(client->callback); - if (client->timeout) { - ev_timer_stop(main_loop, client->timeout); - FREE(client->timeout); - } - - free(client->buffer); - - for (int i = 0; i < client->num_events; i++) { - free(client->events[i]); - } - free(client->events); - TAILQ_REMOVE(&all_clients, client, clients); - free(client); -} - static void ipc_client_timeout(EV_P_ ev_timer *w, int revents); static void ipc_socket_writeable_cb(EV_P_ struct ev_io *w, int revents); @@ -89,8 +49,8 @@ void ipc_set_kill_timeout(ev_tstamp new) { /* * Try to write the contents of the pending buffer to the client's subscription - * socket. Will set, reset or clear the timeout and io callbacks depending on - * the result of the write operation. + * socket. Will set, reset or clear the timeout and io write callbacks depending + * on the result of the write operation. * */ static void ipc_push_pending(ipc_client *client) { @@ -108,13 +68,13 @@ static void ipc_push_pending(ipc_client *client) { ev_timer_stop(main_loop, client->timeout); FREE(client->timeout); } - ev_io_stop(main_loop, client->callback); + ev_io_stop(main_loop, client->write_callback); return; } /* Otherwise, make sure that the io callback is enabled and create a new * timer if needed. */ - ev_io_start(main_loop, client->callback); + ev_io_start(main_loop, client->write_callback); if (!client->timeout) { struct ev_timer *timeout = scalloc(1, sizeof(struct ev_timer)); @@ -140,6 +100,54 @@ static void ipc_push_pending(ipc_client *client) { client->buffer = srealloc(client->buffer, client->buffer_size); } +/* + * Given a message and a message type, create the corresponding header, merge it + * with the message and append it to the given client's output buffer. Also, + * send the message if the client's buffer was empty. + * + */ +static void ipc_send_client_message(ipc_client *client, size_t size, const uint32_t message_type, const uint8_t *payload) { + const i3_ipc_header_t header = { + .magic = {'i', '3', '-', 'i', 'p', 'c'}, + .size = size, + .type = message_type}; + const size_t header_size = sizeof(i3_ipc_header_t); + const size_t message_size = header_size + size; + + const bool push_now = (client->buffer_size == 0); + client->buffer = srealloc(client->buffer, client->buffer_size + message_size); + memcpy(client->buffer + client->buffer_size, ((void *)&header), header_size); + memcpy(client->buffer + client->buffer_size + header_size, payload, size); + client->buffer_size += message_size; + + if (push_now) { + ipc_push_pending(client); + } +} + +static void free_ipc_client(ipc_client *client) { + DLOG("Disconnecting client on fd %d\n", client->fd); + close(client->fd); + + ev_io_stop(main_loop, client->read_callback); + FREE(client->read_callback); + ev_io_stop(main_loop, client->write_callback); + FREE(client->write_callback); + if (client->timeout) { + ev_timer_stop(main_loop, client->timeout); + FREE(client->timeout); + } + + free(client->buffer); + + for (int i = 0; i < client->num_events; i++) { + free(client->events[i]); + } + free(client->events); + TAILQ_REMOVE(&all_clients, client, clients); + free(client); +} + /* * Sends the specified event to all IPC clients which are currently connected * and subscribed to this kind of event. @@ -148,21 +156,11 @@ static void ipc_push_pending(ipc_client *client) { void ipc_send_event(const char *event, uint32_t message_type, const char *payload) { ipc_client *current; TAILQ_FOREACH(current, &all_clients, clients) { - /* see if this client is interested in this event */ - bool interested = false; for (int i = 0; i < current->num_events; i++) { - if (strcasecmp(current->events[i], event) != 0) - continue; - interested = true; - break; - } - if (!interested) - continue; - - const bool push_now = (current->buffer_size == 0); - append_payload(current, message_type, payload); - if (push_now) { - ipc_push_pending(current); + if (strcasecmp(current->events[i], event) == 0) { + ipc_send_client_message(current, strlen(payload), message_type, (uint8_t *)payload); + break; + } } } } @@ -234,8 +232,8 @@ IPC_HANDLER(run_command) { ylength length; yajl_gen_get_buf(gen, &reply, &length); - ipc_send_message(fd, length, I3_IPC_REPLY_TYPE_COMMAND, - (const uint8_t *)reply); + ipc_send_client_message(client, length, I3_IPC_REPLY_TYPE_COMMAND, + (const uint8_t *)reply); yajl_gen_free(gen); } @@ -843,7 +841,7 @@ IPC_HANDLER(tree) { ylength length; y(get_buf, &payload, &length); - ipc_send_message(fd, length, I3_IPC_REPLY_TYPE_TREE, payload); + ipc_send_client_message(client, length, I3_IPC_REPLY_TYPE_TREE, payload); y(free); } @@ -907,7 +905,7 @@ IPC_HANDLER(get_workspaces) { ylength length; y(get_buf, &payload, &length); - ipc_send_message(fd, length, I3_IPC_REPLY_TYPE_WORKSPACES, payload); + ipc_send_client_message(client, length, I3_IPC_REPLY_TYPE_WORKSPACES, payload); y(free); } @@ -961,7 +959,7 @@ IPC_HANDLER(get_outputs) { ylength length; y(get_buf, &payload, &length); - ipc_send_message(fd, length, I3_IPC_REPLY_TYPE_OUTPUTS, payload); + ipc_send_client_message(client, length, I3_IPC_REPLY_TYPE_OUTPUTS, payload); y(free); } @@ -988,7 +986,7 @@ IPC_HANDLER(get_marks) { ylength length; y(get_buf, &payload, &length); - ipc_send_message(fd, length, I3_IPC_REPLY_TYPE_MARKS, payload); + ipc_send_client_message(client, length, I3_IPC_REPLY_TYPE_MARKS, payload); y(free); } @@ -1021,7 +1019,7 @@ IPC_HANDLER(get_version) { ylength length; y(get_buf, &payload, &length); - ipc_send_message(fd, length, I3_IPC_REPLY_TYPE_VERSION, payload); + ipc_send_client_message(client, length, I3_IPC_REPLY_TYPE_VERSION, payload); y(free); } @@ -1046,7 +1044,7 @@ IPC_HANDLER(get_bar_config) { ylength length; y(get_buf, &payload, &length); - ipc_send_message(fd, length, I3_IPC_REPLY_TYPE_BAR_CONFIG, payload); + ipc_send_client_message(client, length, I3_IPC_REPLY_TYPE_BAR_CONFIG, payload); y(free); return; } @@ -1083,7 +1081,7 @@ IPC_HANDLER(get_bar_config) { ylength length; y(get_buf, &payload, &length); - ipc_send_message(fd, length, I3_IPC_REPLY_TYPE_BAR_CONFIG, payload); + ipc_send_client_message(client, length, I3_IPC_REPLY_TYPE_BAR_CONFIG, payload); y(free); } @@ -1105,7 +1103,7 @@ IPC_HANDLER(get_binding_modes) { ylength length; y(get_buf, &payload, &length); - ipc_send_message(fd, length, I3_IPC_REPLY_TYPE_BINDING_MODES, payload); + ipc_send_client_message(client, length, I3_IPC_REPLY_TYPE_BINDING_MODES, payload); y(free); } @@ -1144,21 +1142,6 @@ static int add_subscription(void *extra, const unsigned char *s, IPC_HANDLER(subscribe) { yajl_handle p; yajl_status stat; - ipc_client *current, *client = NULL; - - /* Search the ipc_client structure for this connection */ - TAILQ_FOREACH(current, &all_clients, clients) { - if (current->fd != fd) - continue; - - client = current; - break; - } - - if (client == NULL) { - ELOG("Could not find ipc_client data structure for fd %d\n", fd); - return; - } /* Setup the JSON parser */ static yajl_callbacks callbacks = { @@ -1175,13 +1158,13 @@ IPC_HANDLER(subscribe) { yajl_free_error(p, err); const char *reply = "{\"success\":false}"; - ipc_send_message(fd, strlen(reply), I3_IPC_REPLY_TYPE_SUBSCRIBE, (const uint8_t *)reply); + ipc_send_client_message(client, strlen(reply), I3_IPC_REPLY_TYPE_SUBSCRIBE, (const uint8_t *)reply); yajl_free(p); return; } yajl_free(p); const char *reply = "{\"success\":true}"; - ipc_send_message(fd, strlen(reply), I3_IPC_REPLY_TYPE_SUBSCRIBE, (const uint8_t *)reply); + ipc_send_client_message(client, strlen(reply), I3_IPC_REPLY_TYPE_SUBSCRIBE, (const uint8_t *)reply); if (client->first_tick_sent) { return; @@ -1200,7 +1183,7 @@ IPC_HANDLER(subscribe) { client->first_tick_sent = true; const char *payload = "{\"first\":true,\"payload\":\"\"}"; - ipc_send_message(client->fd, strlen(payload), I3_IPC_EVENT_TICK, (const uint8_t *)payload); + ipc_send_client_message(client, strlen(payload), I3_IPC_EVENT_TICK, (const uint8_t *)payload); } /* @@ -1220,7 +1203,7 @@ IPC_HANDLER(get_config) { ylength length; y(get_buf, &payload, &length); - ipc_send_message(fd, length, I3_IPC_REPLY_TYPE_CONFIG, payload); + ipc_send_client_message(client, length, I3_IPC_REPLY_TYPE_CONFIG, payload); y(free); } @@ -1249,7 +1232,7 @@ IPC_HANDLER(send_tick) { y(free); const char *reply = "{\"success\":true}"; - ipc_send_message(fd, strlen(reply), I3_IPC_REPLY_TYPE_TICK, (const uint8_t *)reply); + ipc_send_client_message(client, strlen(reply), I3_IPC_REPLY_TYPE_TICK, (const uint8_t *)reply); DLOG("Sent tick event\n"); } @@ -1300,7 +1283,7 @@ IPC_HANDLER(sync) { yajl_free_error(p, err); const char *reply = "{\"success\":false}"; - ipc_send_message(fd, strlen(reply), I3_IPC_REPLY_TYPE_SYNC, (const uint8_t *)reply); + ipc_send_client_message(client, strlen(reply), I3_IPC_REPLY_TYPE_SYNC, (const uint8_t *)reply); yajl_free(p); return; } @@ -1309,7 +1292,7 @@ IPC_HANDLER(sync) { DLOG("received IPC sync request (rnd = %d, window = 0x%08x)\n", state.rnd, state.window); sync_respond(state.window, state.rnd); const char *reply = "{\"success\":true}"; - ipc_send_message(fd, strlen(reply), I3_IPC_REPLY_TYPE_SYNC, (const uint8_t *)reply); + ipc_send_client_message(client, strlen(reply), I3_IPC_REPLY_TYPE_SYNC, (const uint8_t *)reply); } /* The index of each callback function corresponds to the numeric @@ -1343,6 +1326,8 @@ static void ipc_receive_message(EV_P_ struct ev_io *w, int revents) { uint32_t message_type; uint32_t message_length; uint8_t *message = NULL; + ipc_client *client = (ipc_client *)w->data; + assert(client->fd == w->fd); int ret = ipc_recv_message(w->fd, &message_type, &message_length, &message); /* EOF or other error */ @@ -1355,25 +1340,8 @@ static void ipc_receive_message(EV_P_ struct ev_io *w, int revents) { /* If not, there was some kind of error. We don’t bother and close the * connection. Delete the client from the list of clients. */ - bool closed = false; - ipc_client *current; - TAILQ_FOREACH(current, &all_clients, clients) { - if (current->fd != w->fd) - continue; - - free_ipc_client(current); - closed = true; - break; - } - if (!closed) { - close(w->fd); - } - - ev_io_stop(EV_A_ w); - free(w); + free_ipc_client(client); FREE(message); - - DLOG("IPC: client disconnected\n"); return; } @@ -1381,7 +1349,7 @@ static void ipc_receive_message(EV_P_ struct ev_io *w, int revents) { DLOG("Unhandled message type: %d\n", message_type); else { handler_t h = handlers[message_type]; - h(w->fd, message, 0, message_length, message_type); + h(client, message, 0, message_length, message_type); } FREE(message); @@ -1453,36 +1421,33 @@ static void ipc_socket_writeable_cb(EV_P_ ev_io *w, int revents) { void ipc_new_client(EV_P_ struct ev_io *w, int revents) { struct sockaddr_un peer; socklen_t len = sizeof(struct sockaddr_un); - int client; - if ((client = accept(w->fd, (struct sockaddr *)&peer, &len)) < 0) { - if (errno == EINTR) - return; - else + int fd; + if ((fd = accept(w->fd, (struct sockaddr *)&peer, &len)) < 0) { + if (errno != EINTR) { perror("accept()"); + } return; } /* Close this file descriptor on exec() */ - (void)fcntl(client, F_SETFD, FD_CLOEXEC); + (void)fcntl(fd, F_SETFD, FD_CLOEXEC); - set_nonblock(client); + set_nonblock(fd); - struct ev_io *package = scalloc(1, sizeof(struct ev_io)); - ev_io_init(package, ipc_receive_message, client, EV_READ); - ev_io_start(EV_A_ package); + ipc_client *client = scalloc(1, sizeof(ipc_client)); + client->fd = fd; - ipc_client *new = scalloc(1, sizeof(ipc_client)); + client->read_callback = scalloc(1, sizeof(struct ev_io)); + client->read_callback->data = client; + ev_io_init(client->read_callback, ipc_receive_message, fd, EV_READ); + ev_io_start(EV_A_ client->read_callback); - package = scalloc(1, sizeof(struct ev_io)); - package->data = new; - ev_io_init(package, ipc_socket_writeable_cb, client, EV_WRITE); + client->write_callback = scalloc(1, sizeof(struct ev_io)); + client->write_callback->data = client; + ev_io_init(client->write_callback, ipc_socket_writeable_cb, fd, EV_WRITE); DLOG("IPC: new client connected on fd %d\n", w->fd); - - new->fd = client; - new->callback = package; - - TAILQ_INSERT_TAIL(&all_clients, new, clients); + TAILQ_INSERT_TAIL(&all_clients, client, clients); } /* From 22ac5827b0d17c227b424a129c6eb0591d2a576a Mon Sep 17 00:00:00 2001 From: nejni-marji Date: Wed, 16 Jan 2019 19:32:24 -0600 Subject: [PATCH 48/98] Make binding modes case sensitive --- src/bindings.c | 2 +- src/config_directives.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bindings.c b/src/bindings.c index 732543d0..65a1821d 100644 --- a/src/bindings.c +++ b/src/bindings.c @@ -621,7 +621,7 @@ void switch_mode(const char *new_mode) { DLOG("Switching to mode %s\n", new_mode); SLIST_FOREACH(mode, &modes, modes) { - if (strcasecmp(mode->name, new_mode) != 0) + if (strcmp(mode->name, new_mode) != 0) continue; ungrab_all_keys(conn); diff --git a/src/config_directives.c b/src/config_directives.c index 0b01d54a..7ca6954c 100644 --- a/src/config_directives.c +++ b/src/config_directives.c @@ -122,7 +122,7 @@ CFGFUN(mode_binding, const char *bindtype, const char *modifiers, const char *ke } CFGFUN(enter_mode, const char *pango_markup, const char *modename) { - if (strcasecmp(modename, DEFAULT_BINDING_MODE) == 0) { + if (strcmp(modename, DEFAULT_BINDING_MODE) == 0) { ELOG("You cannot use the name %s for your mode\n", DEFAULT_BINDING_MODE); return; } From 2a1ca0951e655af23289a45bef30f49a210ebf16 Mon Sep 17 00:00:00 2001 From: Jonathan Woodlief Date: Fri, 18 Jan 2019 17:35:47 -0500 Subject: [PATCH 49/98] Update userguide to describe border styles better Describe the difference between normal and pixel titlebars better Thanks to @JonWoodlief --- docs/userguide | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/userguide b/docs/userguide index 1f947484..9255fd10 100644 --- a/docs/userguide +++ b/docs/userguide @@ -603,6 +603,9 @@ This option determines which border style new windows will have. The default is +normal+. Note that default_floating_border applies only to windows which are starting out as floating windows, e.g., dialog windows, but not windows that are floated later on. +Setting border style to +pixel+ eliminates title bars. The border style +normal+ allows you to +adjust edge border width while keeping your title bar. + *Syntax*: --------------------------------------------- default_border normal|none|pixel From 02bb2693f9a4c676d0e0a9f99bc648a7a4f98058 Mon Sep 17 00:00:00 2001 From: Orestis Date: Tue, 22 Jan 2019 22:35:44 +0200 Subject: [PATCH 50/98] cmd_exit: Let i3_exit handle shutdown (#3600) - __lsan_do_leak_check() will terminate the process, so move it to the end of the function. - ev_loop_destroy() must be called after ipc_shutdown() because the latter calls ev_ functions. Fixes #3599 --- src/commands.c | 10 ---------- src/main.c | 19 ++++++++++++------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/commands.c b/src/commands.c index 9f408eb7..778b4a1a 100644 --- a/src/commands.c +++ b/src/commands.c @@ -13,10 +13,6 @@ #include #include -#ifdef I3_ASAN_ENABLED -#include -#endif - #include "shmlog.h" // Macros to make the YAJL API a bit easier to use. @@ -1573,12 +1569,6 @@ void cmd_layout_toggle(I3_CMD, const char *toggle_mode) { */ void cmd_exit(I3_CMD) { LOG("Exiting due to user command.\n"); -#ifdef I3_ASAN_ENABLED - __lsan_do_leak_check(); -#endif - ipc_shutdown(SHUTDOWN_REASON_EXIT); - unlink(config.ipc_socket_path); - xcb_disconnect(conn); exit(0); /* unreached */ diff --git a/src/main.c b/src/main.c index 074afe71..0db5b440 100644 --- a/src/main.c +++ b/src/main.c @@ -161,13 +161,6 @@ void main_set_x11_cb(bool enable) { * */ static void i3_exit(void) { -/* We need ev >= 4 for the following code. Since it is not *that* important (it - * only makes sure that there are no i3-nagbar instances left behind) we still - * support old systems with libev 3. */ -#if EV_VERSION_MAJOR >= 4 - ev_loop_destroy(main_loop); -#endif - if (*shmlogname != '\0') { fprintf(stderr, "Closing SHM log \"%s\"\n", shmlogname); fflush(stderr); @@ -175,6 +168,18 @@ static void i3_exit(void) { } ipc_shutdown(SHUTDOWN_REASON_EXIT); unlink(config.ipc_socket_path); + xcb_disconnect(conn); + +/* We need ev >= 4 for the following code. Since it is not *that* important (it + * only makes sure that there are no i3-nagbar instances left behind) we still + * support old systems with libev 3. */ +#if EV_VERSION_MAJOR >= 4 + ev_loop_destroy(main_loop); +#endif + +#ifdef I3_ASAN_ENABLED + __lsan_do_leak_check(); +#endif } /* From 85e6742686a997578a98db3230508f2aa2c0e350 Mon Sep 17 00:00:00 2001 From: Alejandro Angulo Date: Thu, 31 Jan 2019 23:00:00 -0800 Subject: [PATCH 51/98] Reword documentation to make clear the difference in enumeration between event and reply types. --- docs/ipc | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/ipc b/docs/ipc index bcf8df1a..22d8e4fe 100644 --- a/docs/ipc +++ b/docs/ipc @@ -681,9 +681,11 @@ responded to. To get informed when certain things happen in i3, clients can subscribe to events. Events consist of a name (like "workspace") and an event reply type -(like I3_IPC_EVENT_WORKSPACE). The events sent by i3 are in the same format -as replies to specific commands. However, the highest bit of the message type -is set to 1 to indicate that this is an event reply instead of a normal reply. +(like I3_IPC_EVENT_WORKSPACE). Events sent by i3 follow a format similar to +replies but with the highest bit of the message type set to 1 to indicate an +event reply instead of a normal reply. Note that event types and reply types +do not follow the same enumeration scheme (e.g. event type 0 corresponds to the +workspace event however reply type 0 corresponds to the COMMAND reply). Caveat: As soon as you subscribe to an event, it is not guaranteed any longer that the requests to i3 are processed in order. This means, the following From 2d6e09a66ad9b3e01588c515ae66476de8903754 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Tue, 12 Feb 2019 09:22:26 +0100 Subject: [PATCH 52/98] i3-dump-log: make log message a little more clear (#3618) This came up when trying to debug an issue. --- i3-dump-log/main.c | 2 +- testcases/t/207-shmlog.t | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/i3-dump-log/main.c b/i3-dump-log/main.c index e9901f8e..4f15f26f 100644 --- a/i3-dump-log/main.c +++ b/i3-dump-log/main.c @@ -155,7 +155,7 @@ int main(int argc, char *argv[]) { exit(1); } if (root_atom_contents("I3_CONFIG_PATH", conn, screen) != NULL) { - fprintf(stderr, "i3-dump-log: ERROR: i3 is running, but SHM logging is not enabled. Enabling SHM log until cancelled\n\n"); + fprintf(stderr, "i3-dump-log: i3 is running, but SHM logging is not enabled. Enabling SHM log now while i3-dump-log is running\n\n"); ipcfd = ipc_connect(NULL); const char *enablecmd = "debuglog on; shmlog 5242880"; if (ipc_send_message(ipcfd, strlen(enablecmd), diff --git a/testcases/t/207-shmlog.t b/testcases/t/207-shmlog.t index c2b2ebaa..94f4bdea 100644 --- a/testcases/t/207-shmlog.t +++ b/testcases/t/207-shmlog.t @@ -31,7 +31,7 @@ run [ 'i3-dump-log' ], '>', \$stdout, '2>', \$stderr; -like($stderr, qr#^i3-dump-log: ERROR: i3 is running, but SHM logging is not enabled\.#, +like($stderr, qr#^i3-dump-log: i3 is running, but SHM logging is not enabled\.#, 'shm logging not enabled'); ################################################################################ @@ -73,7 +73,7 @@ run [ 'i3-dump-log' ], '>', \$stdout, '2>', \$stderr; -like($stderr, qr#^i3-dump-log: ERROR: i3 is running, but SHM logging is not enabled\.#, +like($stderr, qr#^i3-dump-log: i3 is running, but SHM logging is not enabled\.#, 'shm logging not enabled'); done_testing; From 72ccd341fc07476137fea00d67cf7a61d3c99098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20ANDR=C3=89-CHANG?= Date: Sat, 9 Feb 2019 14:22:08 +0000 Subject: [PATCH 53/98] Add proper return code for i3-msg --- i3-msg/main.c | 4 +++- man/i3-msg.man | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/i3-msg/main.c b/i3-msg/main.c index fe111416..0ada5f64 100644 --- a/i3-msg/main.c +++ b/i3-msg/main.c @@ -67,6 +67,7 @@ typedef struct reply_t { char *errorposition; } reply_t; +static int exit_code = 0; static reply_t last_reply; static int reply_boolean_cb(void *params, int val) { @@ -100,6 +101,7 @@ static int reply_end_map_cb(void *params) { fprintf(stderr, "ERROR: %s\n", last_reply.errorposition); } fprintf(stderr, "ERROR: %s\n", last_reply.error); + exit_code = 2; } return 1; } @@ -326,5 +328,5 @@ int main(int argc, char *argv[]) { close(sockfd); - return 0; + return exit_code; } diff --git a/man/i3-msg.man b/man/i3-msg.man index 625131de..ce9b476d 100644 --- a/man/i3-msg.man +++ b/man/i3-msg.man @@ -90,6 +90,15 @@ See the -m option for continuous monitoring. i3-msg is a sample implementation for a client using the unix socket IPC interface to i3. +=== Exit status: + +0: +if OK, +1: +if invalid syntax or unable to connect to ipc-socket +2: +if i3 returned an error processing your command(s) + == EXAMPLES ------------------------------------------------ From 098b0e69763080bad408ed73592b26eecdf1f93e Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Fri, 22 Feb 2019 19:41:30 +0200 Subject: [PATCH 54/98] create_workspace_on_output: send workspace init event Fixes #3595 Like the issue mentions: > instead of the newly created workspace (not referenced by variable > here) the `"init"` event is fired with the current workspace (`ws`). Plus, there was another issue where duplicate workspace init events where being sent because of workspace_get(). 304-ipc-workspace-init.t: Subtest "move workspace to output" fails with current next. Fixes #3631 No event was being sent here: https://github.com/i3/i3/blob/2d6e09a66ad9b3e01588c515ae66476de8903754/src/randr.c#L487 533-randr15.t: I confirmed that SKIP still works if the xrandr command fails. Added test fails with current next. --- src/workspace.c | 9 ++-- testcases/t/304-ipc-workspace-init.t | 69 ++++++++++++++++++++++++++++ testcases/t/533-randr15.t | 12 +++-- 3 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 testcases/t/304-ipc-workspace-init.t diff --git a/src/workspace.c b/src/workspace.c index 2af88d73..5c2c48fa 100644 --- a/src/workspace.c +++ b/src/workspace.c @@ -289,6 +289,7 @@ Con *create_workspace_on_output(Output *output, Con *content) { ws->workspace_layout = config.default_layout; _workspace_apply_default_orientation(ws); + ipc_send_workspace_event("init", ws, NULL); return ws; } @@ -1004,13 +1005,11 @@ bool workspace_move_to_output(Con *ws, Output *output) { break; } - /* if we couldn't create the workspace using an assignment, create - * it on the output */ + /* if we couldn't create the workspace using an assignment, create it on + * the output. Workspace init IPC events are sent either by + * workspace_get or create_workspace_on_output. */ if (!used_assignment) create_workspace_on_output(current_output, ws->parent); - - /* notify the IPC listeners */ - ipc_send_workspace_event("init", ws, NULL); } DLOG("Detaching\n"); diff --git a/testcases/t/304-ipc-workspace-init.t b/testcases/t/304-ipc-workspace-init.t new file mode 100644 index 00000000..0eb838dc --- /dev/null +++ b/testcases/t/304-ipc-workspace-init.t @@ -0,0 +1,69 @@ +#!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 that the workspace init event is correctly sent. +# Ticket: #3631 +# Bug still in: 4.16-85-g2d6e09a6 + +use i3test i3_config => <{change} eq 'init' } @events; + my $len = scalar @init; + is($len, $num_events, "Received $num_events workspace::init event"); + $num_events = $len if $len < $num_events; + for my $idx (0 .. $num_events - 1) { + my $name = shift; + my $output = shift; + is($init[$idx]->{current}->{name}, $name, "workspace name $name matches"); + is($init[$idx]->{current}->{output}, $output, "workspace output $output matches"); + } +} + +subtest 'focus outputs', \&workspace_init_subtest, 'focus output fake-1, focus output fake-0', 0; +subtest 'new workspaces', \&workspace_init_subtest, + 'workspace a, workspace b, workspace a, workspace a', 3, 'a', + 'fake-0', 'b', 'fake-0', 'a', 'fake-0'; +open_window; # Prevent workspace "a" from being deleted. +subtest 'return on existing workspace', \&workspace_init_subtest, + 'workspace a, workspace b, workspace a', 1, 'b', 'fake-0'; +subtest 'assigned workspace is already open', \&workspace_init_subtest, + 'workspace X, workspace b, workspace a', 1, 'b', 'fake-1'; +subtest 'assigned workspace was deleted and now is initialized again', \&workspace_init_subtest, + 'workspace X, workspace b, workspace a', 2, 'X', 'fake-1', 'b', 'fake-1'; +subtest 'move workspace to output', \&workspace_init_subtest, + 'move workspace to output fake-1, move workspace to output fake-0', 2, '1', 'fake-0', 'X', + 'fake-1'; +subtest 'move window to workspace', \&workspace_init_subtest, 'move to workspace b', 1, 'b', + 'fake-0'; +subtest 'back_and_forth', \&workspace_init_subtest, + 'workspace b, workspace back_and_forth, workspace b', 1, + 'a', 'fake-0'; +subtest 'move window to workspace back_and_forth', \&workspace_init_subtest, + 'move window to workspace back_and_forth', 1, 'a', 'fake-0'; + +done_testing; diff --git a/testcases/t/533-randr15.t b/testcases/t/533-randr15.t index 9bbdd74b..51d1c9f6 100644 --- a/testcases/t/533-randr15.t +++ b/testcases/t/533-randr15.t @@ -130,10 +130,16 @@ $tree = i3->get_tree->recv; is_deeply(\@outputs, [ '__i3', 'default' ], 'outputs are __i3 and default'); SKIP: { - skip 'xrandr --setmonitor failed (xrandr too old?)', 1 unless - system(q|xrandr --setmonitor up2414q 3840/527x2160/296+1280+0 none|) == 0; + my @events = events_for( + sub { + skip 'xrandr --setmonitor failed (xrandr too old?)', 1 + unless system(q|xrandr --setmonitor up2414q 3840/527x2160/296+1280+0 none|) == 0; + }, + "workspace"); - sync_with_i3; + my @init = grep { $_->{change} eq 'init' } @events; + is(scalar @init, 1, 'Received 1 workspace::init event'); + is($init[0]->{current}->{output}, 'up2414q', 'Workspace initialized in up2414q'); $tree = i3->get_tree->recv; @outputs = map { $_->{name} } @{$tree->{nodes}}; From 6ec7b91cff21d003aa6eea6df14f3938310bf276 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Tue, 19 Mar 2019 09:30:04 +0100 Subject: [PATCH 55/98] fix travis build by switching away from deprecated-2017Q3 (#3650) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also remove “sudo: false” as per https://blog.travis-ci.com/2018-11-19-required-linux-infrastructure-migration --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8098035c..0133fc71 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,4 @@ -sudo: false dist: trusty -# TODO: remove “group” once trusty kernel is no longer affected by -# https://github.com/google/sanitizers/issues/837 -group: deprecated-2017Q3 services: - docker language: c From bd58d67ea8e020e0f22e577cff461c0820f09244 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Fri, 8 Mar 2019 19:16:39 +0200 Subject: [PATCH 56/98] convert_utf8_to_ucs2: Allow partial conversion Fixes #3638. --- libi3/ucs2_conversion.c | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/libi3/ucs2_conversion.c b/libi3/ucs2_conversion.c index 398c1ae5..c651cdb3 100644 --- a/libi3/ucs2_conversion.c +++ b/libi3/ucs2_conversion.c @@ -69,32 +69,41 @@ xcb_char2b_t *convert_utf8_to_ucs2(char *input, size_t *real_strlen) { xcb_char2b_t *buffer = smalloc(buffer_size); /* We need to use an additional pointer, because iconv() modifies it */ - size_t output_size = buffer_size; + size_t output_bytes_left = buffer_size; xcb_char2b_t *output = buffer; if (ucs2_conversion_descriptor == (iconv_t)-1) { - /* Get a new conversion descriptor */ - ucs2_conversion_descriptor = iconv_open("UCS-2BE", "UTF-8"); - if (ucs2_conversion_descriptor == (iconv_t)-1) + /* Get a new conversion descriptor. //IGNORE is a GNU suffix that makes + * iconv to silently discard characters that cannot be represented in + * the target character set. */ + ucs2_conversion_descriptor = iconv_open("UCS-2BE//IGNORE", "UTF-8"); + if (ucs2_conversion_descriptor == (iconv_t)-1) { + ucs2_conversion_descriptor = iconv_open("UCS-2BE", "UTF-8"); + } + if (ucs2_conversion_descriptor == (iconv_t)-1) { err(EXIT_FAILURE, "Error opening the conversion context"); + } } else { /* Reset the existing conversion descriptor */ iconv(ucs2_conversion_descriptor, NULL, NULL, NULL, NULL); } /* Do the conversion */ - size_t rc = iconv(ucs2_conversion_descriptor, &input, &input_size, (char **)&output, &output_size); + size_t rc = iconv(ucs2_conversion_descriptor, &input, &input_size, + (char **)&output, &output_bytes_left); if (rc == (size_t)-1) { + /* Conversion will only be partial. */ perror("Converting to UCS-2 failed"); - free(buffer); - if (real_strlen != NULL) - *real_strlen = 0; - return NULL; } + /* If no bytes where converted, this is equivalent to freeing buffer. */ + buffer_size -= output_bytes_left; + buffer = srealloc(buffer, buffer_size); + /* Return the resulting string's length */ - if (real_strlen != NULL) - *real_strlen = (buffer_size - output_size) / sizeof(xcb_char2b_t); + if (real_strlen != NULL) { + *real_strlen = buffer_size / sizeof(xcb_char2b_t); + } return buffer; } From 9bd2224520741c743759b81c26b7a65ecf0af6fe Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Tue, 19 Mar 2019 09:49:59 +0100 Subject: [PATCH 57/98] travis: remove deprecated docker login -e flag (#3651) --- travis/docker-build-and-push.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/travis/docker-build-and-push.sh b/travis/docker-build-and-push.sh index 9b654a84..686b81b0 100755 --- a/travis/docker-build-and-push.sh +++ b/travis/docker-build-and-push.sh @@ -15,6 +15,6 @@ docker build --pull --no-cache --rm -t=${BASENAME} -f ${DOCKERFILE} . # the login+push step when the variable isn’t set. if [ -n "${DOCKER_PASS}" ] then - docker login -e ${DOCKER_EMAIL} -u ${DOCKER_USER} -p ${DOCKER_PASS} + docker login -u ${DOCKER_USER} -p ${DOCKER_PASS} docker push ${BASENAME} fi From 79b052230c00d593d9d3f08137f797ace0331bf0 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Thu, 7 Mar 2019 14:58:53 +0100 Subject: [PATCH 58/98] x.c: correctly free con->frame_buffer in _x_con_kill This fixes a crash which I could not reproduce in a testcase with reasonable effort, but the user reported the fix works. Compare with src/x.c:946. fixes #3554 fixes #3645 --- src/x.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/x.c b/src/x.c index f643a9b3..e6d875e5 100644 --- a/src/x.c +++ b/src/x.c @@ -268,6 +268,7 @@ static void _x_con_kill(Con *con) { draw_util_surface_free(conn, &(con->frame)); draw_util_surface_free(conn, &(con->frame_buffer)); xcb_free_pixmap(conn, con->frame_buffer.id); + con->frame_buffer.id = XCB_NONE; state = state_for_frame(con->frame.id); CIRCLEQ_REMOVE(&state_head, state, state); CIRCLEQ_REMOVE(&old_state_head, state, old_state); From 4e5ce56188629634ad35a5aaf24a1f6d506ac19d Mon Sep 17 00:00:00 2001 From: Christopher Hasse Date: Wed, 20 Mar 2019 23:51:13 -0500 Subject: [PATCH 59/98] Add explicit reference to glib2 to automake --- Makefile.am | 2 ++ configure.ac | 1 + 2 files changed, 3 insertions(+) diff --git a/Makefile.am b/Makefile.am index 537fc6a0..cc7fa845 100644 --- a/Makefile.am +++ b/Makefile.am @@ -277,6 +277,7 @@ i3_LDADD = \ libi3_CFLAGS = \ $(AM_CFLAGS) \ + $(GLIBGOBJECT_CFLAGS) \ $(XCB_CFLAGS) \ $(XCB_UTIL_CFLAGS) \ $(XCB_UTIL_XRM_CFLAGS) \ @@ -285,6 +286,7 @@ libi3_CFLAGS = \ libi3_LIBS = \ $(top_builddir)/libi3.a \ + $(GLIBGOBJECT_LIBS) \ $(XCB_LIBS) \ $(XCB_UTIL_LIBS) \ $(XCB_UTIL_XRM_LIBS) \ diff --git a/configure.ac b/configure.ac index b961f61c..23d8d878 100644 --- a/configure.ac +++ b/configure.ac @@ -101,6 +101,7 @@ PKG_CHECK_MODULES([XKBCOMMON], [xkbcommon xkbcommon-x11]) PKG_CHECK_MODULES([YAJL], [yajl]) PKG_CHECK_MODULES([LIBPCRE], [libpcre >= 8.10]) PKG_CHECK_MODULES([PANGOCAIRO], [cairo >= 1.14.4 pangocairo]) +PKG_CHECK_MODULES([GLIBGOBJECT], [glib-2.0 gobject-2.0]) # Checks for programs. AC_PROG_AWK From 8ce99cdacb7e243c7a75e7bbd54de7a7e8300a7e Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Thu, 21 Mar 2019 19:29:56 +0200 Subject: [PATCH 60/98] cfg_workspace: Accept outputs with spaces again This is a regression from bce088679. An other way to fix this would be to concatenate strings inside the strtok loop when an output starts with a double quote but I'd rather let the parser do the word splitting. Fixes #3646 --- parser-specs/config.spec | 10 ++-- src/config_directives.c | 49 ++++++++++++-------- testcases/t/201-config-parser.t | 6 +++ testcases/t/297-assign-workspace-to-output.t | 11 +++-- 4 files changed, 50 insertions(+), 26 deletions(-) diff --git a/parser-specs/config.spec b/parser-specs/config.spec index 43181c59..a256d8b0 100644 --- a/parser-specs/config.spec +++ b/parser-specs/config.spec @@ -277,11 +277,13 @@ state WORKSPACE: state WORKSPACE_OUTPUT: 'output' - -> WORKSPACE_OUTPUT_STR + -> WORKSPACE_OUTPUT_WORD -state WORKSPACE_OUTPUT_STR: - output = string - -> call cfg_workspace($workspace, $output) +state WORKSPACE_OUTPUT_WORD: + output = word + -> call cfg_workspace($workspace, $output); WORKSPACE_OUTPUT_WORD + end + -> INITIAL # ipc-socket state IPC_SOCKET: diff --git a/src/config_directives.c b/src/config_directives.c index 7ca6954c..fb3fba41 100644 --- a/src/config_directives.c +++ b/src/config_directives.c @@ -334,30 +334,41 @@ CFGFUN(show_marks, const char *value) { config.show_marks = eval_boolstr(value); } -CFGFUN(workspace, const char *workspace, const char *outputs) { - DLOG("Assigning workspace \"%s\" to outputs \"%s\"\n", workspace, outputs); - /* Check for earlier assignments of the same workspace so that we - * don’t have assignments of a single workspace to different - * outputs */ +static char *current_workspace = NULL; + +CFGFUN(workspace, const char *workspace, const char *output) { struct Workspace_Assignment *assignment; - TAILQ_FOREACH(assignment, &ws_assignments, ws_assignments) { - if (strcasecmp(assignment->name, workspace) == 0) { - ELOG("You have a duplicate workspace assignment for workspace \"%s\"\n", - workspace); + + /* When a new workspace line is encountered, for the first output word, + * $workspace from the config.spec is non-NULL. Afterwards, the parser calls + * clear_stack() because of the call line. Thus, we have to preserve the + * workspace string. */ + if (workspace) { + FREE(current_workspace); + + TAILQ_FOREACH(assignment, &ws_assignments, ws_assignments) { + if (strcasecmp(assignment->name, workspace) == 0) { + ELOG("You have a duplicate workspace assignment for workspace \"%s\"\n", + workspace); + return; + } + } + + current_workspace = sstrdup(workspace); + } else { + if (!current_workspace) { + DLOG("Both workspace and current_workspace are NULL, assuming we had an error before\n"); return; } + workspace = current_workspace; } - char *buf = sstrdup(outputs); - char *output = strtok(buf, " "); - while (output != NULL) { - assignment = scalloc(1, sizeof(struct Workspace_Assignment)); - assignment->name = sstrdup(workspace); - assignment->output = sstrdup(output); - TAILQ_INSERT_TAIL(&ws_assignments, assignment, ws_assignments); - output = strtok(NULL, " "); - } - free(buf); + DLOG("Assigning workspace \"%s\" to output \"%s\"\n", workspace, output); + + assignment = scalloc(1, sizeof(struct Workspace_Assignment)); + assignment->name = sstrdup(workspace); + assignment->output = sstrdup(output); + TAILQ_INSERT_TAIL(&ws_assignments, assignment, ws_assignments); } CFGFUN(ipc_socket, const char *path) { diff --git a/testcases/t/201-config-parser.t b/testcases/t/201-config-parser.t index c6ce22eb..65997bc7 100644 --- a/testcases/t/201-config-parser.t +++ b/testcases/t/201-config-parser.t @@ -400,12 +400,18 @@ $config = <<'EOT'; workspace 3 output VGA-1 workspace "4: output" output VGA-2 workspace bleh output LVDS1/I_1 +# See #3646 +workspace foo output a b c "a b c" EOT $expected = <<'EOT'; cfg_workspace(3, VGA-1) cfg_workspace(4: output, VGA-2) cfg_workspace(bleh, LVDS1/I_1) +cfg_workspace(foo, a) +cfg_workspace((null), b) +cfg_workspace((null), c) +cfg_workspace((null), a b c) EOT is(parser_calls($config), diff --git a/testcases/t/297-assign-workspace-to-output.t b/testcases/t/297-assign-workspace-to-output.t index a7b75be9..eb8f24b2 100644 --- a/testcases/t/297-assign-workspace-to-output.t +++ b/testcases/t/297-assign-workspace-to-output.t @@ -79,7 +79,9 @@ 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 +workspace foo output doesnotexist1 doesnotexist2 doesnotexist3 +workspace bar output doesnotexist +workspace bar output fake-0 EOT $pid = launch_with_config($config); @@ -91,8 +93,11 @@ 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.'); +cmd 'focus output fake-0, workspace foo'; +check_output('foo', 'fake-0', 'Workspace with only non-existing assigned outputs opened in current output'); + +cmd 'focus output fake-0, workspace bar'; +check_output('bar', 'fake-0', 'Second workspace assignment line ignored'); # Moving assigned workspaces. cmd 'workspace 2, move workspace to output left'; From 214ed78154ab4b92ec78eb48adb0a219083a13c5 Mon Sep 17 00:00:00 2001 From: Iskustvo Date: Mon, 11 Mar 2019 23:45:35 +0100 Subject: [PATCH 61/98] Added new IPC library(i3-ipc++) in documents. --- docs/ipc | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/ipc b/docs/ipc index 22d8e4fe..f25d7a74 100644 --- a/docs/ipc +++ b/docs/ipc @@ -950,6 +950,7 @@ C:: * i3 includes a headerfile +i3/ipc.h+ which provides you all constants. * https://github.com/acrisci/i3ipc-glib C++:: + * https://github.com/Iskustvo/i3-ipcpp[i3-ipc++] * https://github.com/drmgc/i3ipcpp Go:: * https://github.com/mdirkse/i3ipc-go From ac2489240ea74a0358df607227b58e74c2e95e59 Mon Sep 17 00:00:00 2001 From: Jeffrey Huxen Date: Thu, 21 Mar 2019 17:07:23 -0500 Subject: [PATCH 62/98] Added to note to clarify which config directives could be used at runtime. --- docs/userguide | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/userguide b/docs/userguide index 9255fd10..a6cd29d5 100644 --- a/docs/userguide +++ b/docs/userguide @@ -680,7 +680,8 @@ for_window [class="urxvt"] border pixel 1 for_window [title="x200: ~/work"] floating enable ------------------------------------------------ -The valid criteria are the same as those for commands, see <>. +The valid criteria are the same as those for commands, see <>. Only config +directives with a command equivalent can be executed at runtime, see <>. [[no_focus]] === Don't focus window upon opening @@ -1846,6 +1847,9 @@ The criteria +class+, +instance+, +role+, +title+, +workspace+ and +mark+ are actually regular expressions (PCRE). See +pcresyntax(3)+ or +perldoc perlre+ for information on how to use them. +Note that config directives listed under <> cannot be changed at runtime +unless they happen to have a command equivalent. + [[exec]] === Executing applications (exec) From d0ab51db85f0176d51bafe4d03f9c7c1621cf53c Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Thu, 21 Mar 2019 22:48:39 +0200 Subject: [PATCH 63/98] workspace_move_to_output: Make stylistic changes --- src/workspace.c | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/workspace.c b/src/workspace.c index 5c2c48fa..b36885ee 100644 --- a/src/workspace.c +++ b/src/workspace.c @@ -964,7 +964,7 @@ Con *workspace_encapsulate(Con *ws) { * This returns true if and only if moving the workspace was successful. */ bool workspace_move_to_output(Con *ws, Output *output) { - LOG("Trying to move workspace %p / %s to output %p / \"%s\".\n", ws, ws->name, output, output_primary_name(output)); + DLOG("Moving workspace %p / %s to output %p / \"%s\".\n", ws, ws->name, output, output_primary_name(output)); Output *current_output = get_output_for_con(ws); if (current_output == NULL) { @@ -973,7 +973,7 @@ bool workspace_move_to_output(Con *ws, Output *output) { } Con *content = output_get_content(output->con); - LOG("got output %p with content %p\n", output, content); + DLOG("got output %p with content %p\n", output, content); Con *previously_visible_ws = TAILQ_FIRST(&(content->focus_head)); if (previously_visible_ws) { @@ -984,7 +984,7 @@ bool workspace_move_to_output(Con *ws, Output *output) { bool workspace_was_visible = workspace_is_visible(ws); if (con_num_children(ws->parent) == 1) { - LOG("Creating a new workspace to replace \"%s\" (last on its output).\n", ws->name); + DLOG("Creating a new workspace to replace \"%s\" (last on its output).\n", ws->name); /* check if we can find a workspace assigned to this output */ bool used_assignment = false; @@ -999,7 +999,7 @@ bool workspace_move_to_output(Con *ws, Output *output) { } /* so create the workspace referenced to by this assignment */ - LOG("Creating workspace from assignment %s.\n", assignment->name); + DLOG("Creating workspace from assignment %s.\n", assignment->name); workspace_get(assignment->name, NULL); used_assignment = true; break; @@ -1008,8 +1008,9 @@ bool workspace_move_to_output(Con *ws, Output *output) { /* if we couldn't create the workspace using an assignment, create it on * the output. Workspace init IPC events are sent either by * workspace_get or create_workspace_on_output. */ - if (!used_assignment) + if (!used_assignment) { create_workspace_on_output(current_output, ws->parent); + } } DLOG("Detaching\n"); @@ -1017,18 +1018,19 @@ bool workspace_move_to_output(Con *ws, Output *output) { Con *old_content = ws->parent; con_detach(ws); if (workspace_was_visible) { - /* The workspace which we just detached was visible, so focus - * the next one in the focus-stack. */ + /* The workspace which we just detached was visible, so focus the next + * one in the focus-stack. */ Con *focus_ws = TAILQ_FIRST(&(old_content->focus_head)); - LOG("workspace was visible, focusing %p / %s now\n", focus_ws, focus_ws->name); + DLOG("workspace was visible, focusing %p / %s now\n", focus_ws, focus_ws->name); workspace_show(focus_ws); } con_attach(ws, content, false); /* fix the coordinates of the floating containers */ Con *floating_con; - TAILQ_FOREACH(floating_con, &(ws->floating_head), floating_windows) - floating_fix_coordinates(floating_con, &(old_content->rect), &(content->rect)); + TAILQ_FOREACH(floating_con, &(ws->floating_head), floating_windows) { + floating_fix_coordinates(floating_con, &(old_content->rect), &(content->rect)); + } ipc_send_workspace_event("move", ws, NULL); if (workspace_was_visible) { @@ -1040,18 +1042,19 @@ bool workspace_move_to_output(Con *ws, Output *output) { return true; } - /* NB: We cannot simply work with previously_visible_ws since it might - * have been cleaned up by workspace_show() already, depending on the - * focus order/number of other workspaces on the output. - * Instead, we loop through the available workspaces and only work with - * previously_visible_ws if we still find it. */ + /* NB: We cannot simply work with previously_visible_ws since it might have + * been cleaned up by workspace_show() already, depending on the focus + * order/number of other workspaces on the output. Instead, we loop through + * the available workspaces and only work with previously_visible_ws if we + * still find it. */ TAILQ_FOREACH(ws, &(content->nodes_head), nodes) { - if (ws != previously_visible_ws) + if (ws != previously_visible_ws) { continue; + } /* Call the on_remove_child callback of the workspace which previously - * was visible on the destination output. Since it is no longer - * visible, it might need to get cleaned up. */ + * was visible on the destination output. Since it is no longer visible, + * it might need to get cleaned up. */ CALL(previously_visible_ws, on_remove_child); break; } From 351d891f4cfdc97189898025ce4eed3eec31afab Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Thu, 21 Mar 2019 23:51:57 +0200 Subject: [PATCH 64/98] get_output_for_con: Assert result != NULL - The result from con_get_output was always not NULL because con_get_output asserts so - get_output_by_name should always be able to get an output from the corresponding container - workspace_move_to_output doesn't return bool anymore since it can't fail --- include/workspace.h | 3 +-- src/commands.c | 11 +---------- src/con.c | 2 -- src/output.c | 10 +--------- src/workspace.c | 12 ++---------- 5 files changed, 5 insertions(+), 33 deletions(-) diff --git a/include/workspace.h b/include/workspace.h index db544ce8..69974a2e 100644 --- a/include/workspace.h +++ b/include/workspace.h @@ -218,7 +218,6 @@ Con *workspace_encapsulate(Con *ws); /** * Move the given workspace to the specified output. - * This returns true if and only if moving the workspace was successful. * */ -bool workspace_move_to_output(Con *ws, Output *output); +void workspace_move_to_output(Con *ws, Output *output); diff --git a/src/commands.c b/src/commands.c index 778b4a1a..b7fbce90 100644 --- a/src/commands.c +++ b/src/commands.c @@ -1114,22 +1114,13 @@ void cmd_move_workspace_to_output(I3_CMD, const char *name) { } Output *current_output = get_output_for_con(ws); - if (current_output == NULL) { - yerror("Cannot get current output. This is a bug in i3."); - return; - } - Output *target_output = get_output_from_string(current_output, name); if (!target_output) { yerror("Could not get output from string \"%s\"", name); return; } - bool success = workspace_move_to_output(ws, target_output); - if (!success) { - yerror("Failed to move workspace to output."); - return; - } + workspace_move_to_output(ws, target_output); } cmd_output->needs_tree_render = true; diff --git a/src/con.c b/src/con.c index f904bfe8..a88909e1 100644 --- a/src/con.c +++ b/src/con.c @@ -1400,8 +1400,6 @@ void con_move_to_output(Con *con, Output *output, bool fix_coordinates) { */ bool con_move_to_output_name(Con *con, const char *name, bool fix_coordinates) { Output *current_output = get_output_for_con(con); - assert(current_output != NULL); - Output *output = get_output_from_string(current_output, name); if (output == NULL) { ELOG("Could not find output \"%s\"\n", name); diff --git a/src/output.c b/src/output.c index ebba5c77..3d931b1b 100644 --- a/src/output.c +++ b/src/output.c @@ -54,16 +54,8 @@ char *output_primary_name(Output *output) { Output *get_output_for_con(Con *con) { Con *output_con = con_get_output(con); - if (output_con == NULL) { - ELOG("Could not get the output container for con = %p.\n", con); - return NULL; - } - Output *output = get_output_by_name(output_con->name, true); - if (output == NULL) { - ELOG("Could not get output from name \"%s\".\n", output_con->name); - return NULL; - } + assert(output != NULL); return output; } diff --git a/src/workspace.c b/src/workspace.c index b36885ee..0aeecb45 100644 --- a/src/workspace.c +++ b/src/workspace.c @@ -961,17 +961,11 @@ Con *workspace_encapsulate(Con *ws) { /* * Move the given workspace to the specified output. - * This returns true if and only if moving the workspace was successful. */ -bool workspace_move_to_output(Con *ws, Output *output) { +void workspace_move_to_output(Con *ws, Output *output) { DLOG("Moving workspace %p / %s to output %p / \"%s\".\n", ws, ws->name, output, output_primary_name(output)); Output *current_output = get_output_for_con(ws); - if (current_output == NULL) { - ELOG("Cannot get current output. This is a bug in i3.\n"); - return false; - } - Con *content = output_get_content(output->con); DLOG("got output %p with content %p\n", output, content); @@ -1039,7 +1033,7 @@ bool workspace_move_to_output(Con *ws, Output *output) { } if (!previously_visible_ws) { - return true; + return; } /* NB: We cannot simply work with previously_visible_ws since it might have @@ -1058,6 +1052,4 @@ bool workspace_move_to_output(Con *ws, Output *output) { CALL(previously_visible_ws, on_remove_child); break; } - - return true; } From 7fc3bf660e58ad11908be60d7df048f920e74689 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Thu, 21 Mar 2019 23:57:24 +0200 Subject: [PATCH 65/98] cmd_focus_output: Avoid assertion crash Happened when the command criteria didn't match any windows. For example: `[con_mark=doesnotexist] focus output left`. --- src/commands.c | 21 +++++++-------------- testcases/t/502-focus-output.t | 29 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/commands.c b/src/commands.c index b7fbce90..3cf5a57c 100644 --- a/src/commands.c +++ b/src/commands.c @@ -1626,24 +1626,18 @@ void cmd_open(I3_CMD) { * */ void cmd_focus_output(I3_CMD, const char *name) { - owindow *current; - - DLOG("name = %s\n", name); - HANDLE_EMPTY_MATCH; - /* get the output */ - Output *current_output = NULL; - Output *output; + if (TAILQ_EMPTY(&owindows)) { + ysuccess(true); + return; + } - TAILQ_FOREACH(current, &owindows, owindows) - current_output = get_output_for_con(current->con); - assert(current_output != NULL); - - output = get_output_from_string(current_output, name); + Output *current_output = get_output_for_con(TAILQ_FIRST(&owindows)->con); + Output *output = get_output_from_string(current_output, name); if (!output) { - yerror("No such output found."); + yerror("Output %s not found.", name); return; } @@ -1658,7 +1652,6 @@ void cmd_focus_output(I3_CMD, const char *name) { workspace_show(ws); cmd_output->needs_tree_render = true; - // XXX: default reply for now, make this a better reply ysuccess(true); } diff --git a/testcases/t/502-focus-output.t b/testcases/t/502-focus-output.t index 01d8db33..118aba16 100644 --- a/testcases/t/502-focus-output.t +++ b/testcases/t/502-focus-output.t @@ -86,4 +86,33 @@ is(focused_output, 'fake-1', 'focus on second output'); cmd 'focus output fake-0'; is(focused_output, 'fake-0', 'focus on first output'); +################################################################################ +# use 'focus output' with command criteria and verify that i3 does not crash +# when they don't match any window +################################################################################ + +is(focused_output, 'fake-0', 'focus on first output'); + +cmd '[con_mark=doesnotexist] focus output right'; +does_i3_live; +is(focused_output, 'fake-0', 'focus remained on first output'); + +################################################################################ +# use 'focus output' with command criteria and verify that focus gets changed +# appropriately +################################################################################ + +is(focused_output, 'fake-0', 'focus on first output'); + +my $window = open_window; + +cmd 'focus output right'; +is(focused_output, 'fake-1', 'focus on second output'); + +cmd '[id= . ' . $window->id . '] focus output right'; +is(focused_output, 'fake-1', 'focus on second output after command with criteria'); + +cmd 'focus output right'; +is(focused_output, 'fake-0', 'focus on first output after command without criteria'); + done_testing; From d4e4cbfd2504fee3a2570bac5b1c1104cb5f2e2b Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Fri, 22 Mar 2019 00:55:30 +0200 Subject: [PATCH 66/98] workspace_move_to_output: Avoid operations when workspace already at destination Closes #3635. Probably the bug can still happen when a tree_close_internal happens inside a workspace_show but modifying the code to avoid them seems to not be worth it. --- src/workspace.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/workspace.c b/src/workspace.c index 0aeecb45..3cf74754 100644 --- a/src/workspace.c +++ b/src/workspace.c @@ -969,6 +969,11 @@ void workspace_move_to_output(Con *ws, Output *output) { Con *content = output_get_content(output->con); DLOG("got output %p with content %p\n", output, content); + if (ws->parent == content) { + DLOG("Nothing to do, workspace already there\n"); + return; + } + Con *previously_visible_ws = TAILQ_FIRST(&(content->focus_head)); if (previously_visible_ws) { DLOG("Previously visible workspace = %p / %s\n", previously_visible_ws, previously_visible_ws->name); From fefeedf8daddd39c9c1428052c7c1a809077c612 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Fri, 22 Mar 2019 15:06:23 +0200 Subject: [PATCH 67/98] Use git plumbing commands to get the I3_VERSION Fixes #3656 --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 23d8d878..ec1d9d03 100644 --- a/configure.ac +++ b/configure.ac @@ -32,7 +32,7 @@ AX_EXTEND_SRCDIR AS_IF([test -d ${srcdir}/.git], [ VERSION="$(git -C ${srcdir} describe --tags --abbrev=0)" - I3_VERSION="$(git -C ${srcdir} describe --tags --always) ($(git -C ${srcdir} log --pretty=format:%cd --date=short -n1), branch \\\"$(git -C ${srcdir} describe --tags --always --all | sed s:heads/::)\\\")" + I3_VERSION="$(git -C ${srcdir} describe --tags --always) ($(git -C ${srcdir} rev-list --format=%cd --date=short -n1 $(git rev-parse HEAD) | tail -n1), branch \\\"$(git -C ${srcdir} describe --tags --always --all | sed s:heads/::)\\\")" # Mirrors what libi3/is_debug_build.c does: is_release=$(test $(echo "${I3_VERSION}" | cut -d '(' -f 1 | wc -m) -lt 10 && echo yes || echo no) ], From ae16a55616b39d68f9afb6f07a6fab87d6b13b36 Mon Sep 17 00:00:00 2001 From: lasers Date: Thu, 28 Mar 2019 11:42:09 -0500 Subject: [PATCH 68/98] docs/i3bar-protocol: add markup to all possible entries example --- docs/i3bar-protocol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/i3bar-protocol b/docs/i3bar-protocol index 826cae53..10ee795b 100644 --- a/docs/i3bar-protocol +++ b/docs/i3bar-protocol @@ -221,7 +221,8 @@ An example of a block which uses all possible entries follows: "name": "ethernet", "instance": "eth0", "separator": true, - "separator_block_width": 9 + "separator_block_width": 9, + "markup": "none" } ------------------------------------------ From 03d2ccdeefc67ff213da81b5dca2846745476660 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Thu, 28 Mar 2019 19:57:32 +0200 Subject: [PATCH 69/98] handle_button: Use full render width for calculations Fixes #3664 Also, click events' width will now always be >= min_width. --- i3bar/src/xcb.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/i3bar/src/xcb.c b/i3bar/src/xcb.c index 31ae08f0..89b658d5 100644 --- a/i3bar/src/xcb.c +++ b/i3bar/src/xcb.c @@ -521,15 +521,16 @@ static void handle_button(xcb_button_press_event_t *event) { if (i3string_get_num_bytes(text) == 0) continue; + const uint32_t full_render_width = render->width + render->x_offset + render->x_append; const int relative_x = statusline_x - last_block_x; - if (relative_x >= 0 && (uint32_t)relative_x <= render->width) { + if (relative_x >= 0 && (uint32_t)relative_x <= full_render_width) { send_block_clicked(event->detail, block->name, block->instance, - event->root_x, event->root_y, relative_x, event->event_y, render->width, bar_height, + event->root_x, event->root_y, relative_x, event->event_y, full_render_width, bar_height, event->state); return; } - last_block_x += render->width + render->x_append + render->x_offset + block->sep_block_width; + last_block_x += full_render_width + block->sep_block_width; } } } From c2a1b6e91f02122b9a37c8d63f209f8275b0ea27 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Thu, 28 Mar 2019 20:55:32 +0200 Subject: [PATCH 70/98] handle_button: Introduce child_handle_button Also fixes an issue where action would be called if the button press was on a separator. For example, if a user scrolled on a separator, the workspace would change. Applies to --release commands as well. --- i3bar/src/xcb.c | 100 ++++++++++++++++++++++++++++++------------------ 1 file changed, 62 insertions(+), 38 deletions(-) diff --git a/i3bar/src/xcb.c b/i3bar/src/xcb.c index 89b658d5..fb331a08 100644 --- a/i3bar/src/xcb.c +++ b/i3bar/src/xcb.c @@ -454,6 +454,61 @@ static bool execute_custom_command(xcb_keycode_t input_code, bool event_is_relea return false; } +static void child_handle_button(xcb_button_press_event_t *event, i3_output *output, int32_t x) { + if (!child_want_click_events()) { + return; + } + + const int tray_width = get_tray_width(output->trayclients); + /* Calculate the horizontal coordinate (x) of the start of the statusline by + * subtracting its width and the width of the tray from the bar width. */ + const int offset = output->rect.w - output->statusline_width - tray_width - logical_px((tray_width > 0) * sb_hoff_px); + /* x of the click event relative to the start of the statusline. */ + const uint32_t statusline_x = x - offset; + + if (x < offset || statusline_x > (uint32_t)output->statusline_width) { + return; + } + + /* x of the start of the current block relative to the statusline. */ + uint32_t last_block_x = 0; + struct status_block *block; + TAILQ_FOREACH(block, &statusline_head, blocks) { + i3String *text; + struct status_block_render_desc *render; + if (output->statusline_short_text && block->short_text != NULL) { + text = block->short_text; + render = &block->short_render; + } else { + text = block->full_text; + render = &block->full_render; + } + + if (i3string_get_num_bytes(text) == 0) { + continue; + } + + /* Include the whole block in our calculations: when min_width is + * specified, we have to take padding width into account. */ + const uint32_t full_render_width = render->width + render->x_offset + render->x_append; + /* x of the click event relative to the current block. */ + const uint32_t relative_x = statusline_x - last_block_x; + if (relative_x <= full_render_width) { + send_block_clicked(event->detail, block->name, block->instance, + event->root_x, event->root_y, relative_x, + event->event_y, full_render_width, bar_height, + event->state); + return; + } + + last_block_x += full_render_width + block->sep_block_width; + if (last_block_x > statusline_x) { + /* Click was on a separator. */ + return; + } + } +} + /* * Handle a button press event (i.e. a mouse click on one of our bars). * We determine, whether the click occurred on a workspace button or if the scroll- @@ -479,10 +534,6 @@ static void handle_button(xcb_button_press_event_t *event) { /* During button release events, only check for custom commands. */ const bool event_is_release = (event->response_type & ~0x80) == XCB_BUTTON_RELEASE; - if (event_is_release) { - execute_custom_command(event->detail, event_is_release); - return; - } int32_t x = event->event_x >= 0 ? event->event_x : 0; int workspace_width = 0; @@ -499,45 +550,18 @@ static void handle_button(xcb_button_press_event_t *event) { workspace_width += logical_px(ws_spacing_px); } - if (x > workspace_width && child_want_click_events()) { - /* If the child asked for click events, - * check if a status block has been clicked. */ - int tray_width = get_tray_width(walk->trayclients); - int last_block_x = 0; - int offset = walk->rect.w - walk->statusline_width - tray_width - logical_px((tray_width > 0) * sb_hoff_px); - int32_t statusline_x = x - offset; - - if (statusline_x >= 0 && statusline_x < walk->statusline_width) { - struct status_block *block; - - TAILQ_FOREACH(block, &statusline_head, blocks) { - i3String *text = block->full_text; - struct status_block_render_desc *render = &block->full_render; - if (walk->statusline_short_text && block->short_text != NULL) { - text = block->short_text; - render = &block->short_render; - } - - if (i3string_get_num_bytes(text) == 0) - continue; - - const uint32_t full_render_width = render->width + render->x_offset + render->x_append; - const int relative_x = statusline_x - last_block_x; - if (relative_x >= 0 && (uint32_t)relative_x <= full_render_width) { - send_block_clicked(event->detail, block->name, block->instance, - event->root_x, event->root_y, relative_x, event->event_y, full_render_width, bar_height, - event->state); - return; - } - - last_block_x += full_render_width + block->sep_block_width; - } + if (x > workspace_width) { + if (!event_is_release) { + child_handle_button(event, walk, x); } + /* Return to avoid executing any other actions when a separator is + * clicked. */ + return; } /* If a custom command was specified for this mouse button, it overrides * the default behavior. */ - if (execute_custom_command(event->detail, event_is_release)) { + if (execute_custom_command(event->detail, event_is_release) || event_is_release) { return; } From 6f1350865b83b856b2b9838920e12661d132af21 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Thu, 28 Mar 2019 21:29:09 +0200 Subject: [PATCH 71/98] etc/config: Mention ~/.config/i3/config --- etc/config | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/etc/config b/etc/config index da51d570..f6f2f9db 100644 --- a/etc/config +++ b/etc/config @@ -187,7 +187,8 @@ bar { # keysym-based config which used their favorite modifier (alt or windows) # # i3-config-wizard will not launch if there already is a config file -# in ~/.i3/config. +# in ~/.config/i3/config (or $XDG_CONFIG_HOME/i3/config if set) or +# ~/.i3/config. # # Please remove the following exec line: ####################################################################### From f9c4011691e808d7591920c034e8449c24249377 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Fri, 29 Mar 2019 01:59:26 +0200 Subject: [PATCH 72/98] Update configuration.h - parse_configuration was mentioning outdated config file order - kill_configerror_nagbar was not used anywhere --- include/configuration.h | 13 +------------ src/config.c | 3 +++ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/include/configuration.h b/include/configuration.h index 6f55ac2a..65c08a8e 100644 --- a/include/configuration.h +++ b/include/configuration.h @@ -415,7 +415,7 @@ struct tray_output_t { bool parse_configuration(const char *override_configpath, bool use_nagbar); /** - * Reads the configuration from ~/.i3/config or /etc/i3/config if not found. + * (Re-)loads the configuration file (sets useful defaults before). * * If you specify override_configpath, only this path is used to look for a * configuration file. @@ -435,14 +435,3 @@ void ungrab_all_keys(xcb_connection_t *conn); * */ void update_barconfig(void); - -/** - * Kills the configerror i3-nagbar process, if any. - * - * Called when reloading/restarting. - * - * If wait_for_it is set (restarting), this function will waitpid(), otherwise, - * ev is assumed to handle it (reloading). - * - */ -void kill_configerror_nagbar(bool wait_for_it); diff --git a/src/config.c b/src/config.c index 402771b1..9787d985 100644 --- a/src/config.c +++ b/src/config.c @@ -69,6 +69,9 @@ bool parse_configuration(const char *override_configpath, bool use_nagbar) { /* * (Re-)loads the configuration file (sets useful defaults before). * + * If you specify override_configpath, only this path is used to look for a + * configuration file. + * */ void load_configuration(xcb_connection_t *conn, const char *override_configpath, bool reload) { if (reload) { From 4a2cacebf69d34ffd272871ffdfbe56028d8f7c2 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Fri, 29 Mar 2019 02:15:49 +0200 Subject: [PATCH 73/98] load_configuration: Remove conn argument --- include/configuration.h | 2 +- src/commands.c | 2 +- src/config.c | 2 +- src/main.c | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/configuration.h b/include/configuration.h index 65c08a8e..1971146b 100644 --- a/include/configuration.h +++ b/include/configuration.h @@ -421,7 +421,7 @@ bool parse_configuration(const char *override_configpath, bool use_nagbar); * configuration file. * */ -void load_configuration(xcb_connection_t *conn, const char *override_configfile, bool reload); +void load_configuration(const char *override_configfile, bool reload); /** * Ungrabs all keys, to be called before re-grabbing the keys because of a diff --git a/src/commands.c b/src/commands.c index 3cf5a57c..69015d92 100644 --- a/src/commands.c +++ b/src/commands.c @@ -1573,7 +1573,7 @@ void cmd_reload(I3_CMD) { LOG("reloading\n"); kill_nagbar(&config_error_nagbar_pid, false); kill_nagbar(&command_error_nagbar_pid, false); - load_configuration(conn, NULL, true); + load_configuration(NULL, true); x_set_i3_atoms(); /* Send an IPC event just in case the ws names have changed */ ipc_send_workspace_event("reload", NULL, NULL); diff --git a/src/config.c b/src/config.c index 9787d985..22cdc096 100644 --- a/src/config.c +++ b/src/config.c @@ -73,7 +73,7 @@ bool parse_configuration(const char *override_configpath, bool use_nagbar) { * configuration file. * */ -void load_configuration(xcb_connection_t *conn, const char *override_configpath, bool reload) { +void load_configuration(const char *override_configpath, bool reload) { if (reload) { /* If we are currently in a binding mode, we first revert to the * default since we have no guarantee that the current mode will even diff --git a/src/main.c b/src/main.c index 0db5b440..d7d7530a 100644 --- a/src/main.c +++ b/src/main.c @@ -586,7 +586,7 @@ int main(int argc, char *argv[]) { #include "atoms.xmacro" #undef xmacro - load_configuration(conn, override_configpath, false); + load_configuration(override_configpath, false); if (config.ipc_socket_path == NULL) { /* Fall back to a file name in /tmp/ based on the PID */ From 7754de900aa41eeedf6d3ce4d661b550b7a60177 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Fri, 29 Mar 2019 11:41:27 +0200 Subject: [PATCH 74/98] Move code clearing the config to a new function --- src/config.c | 236 ++++++++++++++++++++++++++------------------------- 1 file changed, 121 insertions(+), 115 deletions(-) diff --git a/src/config.c b/src/config.c index 22cdc096..e0eb5378 100644 --- a/src/config.c +++ b/src/config.c @@ -39,6 +39,126 @@ void update_barconfig(void) { } } +static void free_configuration(void) { + assert(conn != NULL); + + /* If we are currently in a binding mode, we first revert to the default + * since we have no guarantee that the current mode will even still exist + * after parsing the config again. See #2228. */ + switch_mode("default"); + + /* First ungrab the keys */ + ungrab_all_keys(conn); + + struct Mode *mode; + while (!SLIST_EMPTY(&modes)) { + mode = SLIST_FIRST(&modes); + FREE(mode->name); + + /* Clear the old binding list */ + while (!TAILQ_EMPTY(mode->bindings)) { + Binding *bind = TAILQ_FIRST(mode->bindings); + TAILQ_REMOVE(mode->bindings, bind, bindings); + binding_free(bind); + } + FREE(mode->bindings); + + SLIST_REMOVE(&modes, mode, Mode, modes); + FREE(mode); + } + + while (!TAILQ_EMPTY(&assignments)) { + struct Assignment *assign = TAILQ_FIRST(&assignments); + if (assign->type == A_TO_WORKSPACE || assign->type == A_TO_WORKSPACE_NUMBER) + FREE(assign->dest.workspace); + else if (assign->type == A_COMMAND) + FREE(assign->dest.command); + else if (assign->type == A_TO_OUTPUT) + FREE(assign->dest.output); + match_free(&(assign->match)); + TAILQ_REMOVE(&assignments, assign, assignments); + FREE(assign); + } + + while (!TAILQ_EMPTY(&ws_assignments)) { + struct Workspace_Assignment *assign = TAILQ_FIRST(&ws_assignments); + FREE(assign->name); + FREE(assign->output); + TAILQ_REMOVE(&ws_assignments, assign, ws_assignments); + FREE(assign); + } + + /* Clear bar configs */ + Barconfig *barconfig; + while (!TAILQ_EMPTY(&barconfigs)) { + barconfig = TAILQ_FIRST(&barconfigs); + FREE(barconfig->id); + for (int c = 0; c < barconfig->num_outputs; c++) + free(barconfig->outputs[c]); + + while (!TAILQ_EMPTY(&(barconfig->bar_bindings))) { + struct Barbinding *binding = TAILQ_FIRST(&(barconfig->bar_bindings)); + FREE(binding->command); + TAILQ_REMOVE(&(barconfig->bar_bindings), binding, bindings); + FREE(binding); + } + + while (!TAILQ_EMPTY(&(barconfig->tray_outputs))) { + struct tray_output_t *tray_output = TAILQ_FIRST(&(barconfig->tray_outputs)); + FREE(tray_output->output); + TAILQ_REMOVE(&(barconfig->tray_outputs), tray_output, tray_outputs); + FREE(tray_output); + } + + FREE(barconfig->outputs); + FREE(barconfig->socket_path); + FREE(barconfig->status_command); + FREE(barconfig->i3bar_command); + FREE(barconfig->font); + FREE(barconfig->colors.background); + FREE(barconfig->colors.statusline); + FREE(barconfig->colors.separator); + FREE(barconfig->colors.focused_background); + FREE(barconfig->colors.focused_statusline); + FREE(barconfig->colors.focused_separator); + FREE(barconfig->colors.focused_workspace_border); + FREE(barconfig->colors.focused_workspace_bg); + FREE(barconfig->colors.focused_workspace_text); + FREE(barconfig->colors.active_workspace_border); + FREE(barconfig->colors.active_workspace_bg); + FREE(barconfig->colors.active_workspace_text); + FREE(barconfig->colors.inactive_workspace_border); + FREE(barconfig->colors.inactive_workspace_bg); + FREE(barconfig->colors.inactive_workspace_text); + FREE(barconfig->colors.urgent_workspace_border); + FREE(barconfig->colors.urgent_workspace_bg); + FREE(barconfig->colors.urgent_workspace_text); + FREE(barconfig->colors.binding_mode_border); + FREE(barconfig->colors.binding_mode_bg); + FREE(barconfig->colors.binding_mode_text); + TAILQ_REMOVE(&barconfigs, barconfig, configs); + FREE(barconfig); + } + + Con *con; + TAILQ_FOREACH(con, &all_cons, all_cons) { + /* Assignments changed, previously ran assignments are invalid. */ + if (con->window) { + con->window->nr_assignments = 0; + FREE(con->window->ran_assignments); + } + /* Invalidate pixmap caches in case font or colors changed. */ + FREE(con->deco_render_params); + } + + /* Get rid of the current font */ + free_font(); + + free(config.ipc_socket_path); + free(config.restart_state_path); + free(config.fake_outputs); +} + /* * Finds the configuration file to use (either the one specified by * override_configpath), the user’s one or the system default) and calls @@ -75,121 +195,7 @@ bool parse_configuration(const char *override_configpath, bool use_nagbar) { */ void load_configuration(const char *override_configpath, bool reload) { if (reload) { - /* If we are currently in a binding mode, we first revert to the - * default since we have no guarantee that the current mode will even - * still exist after parsing the config again. See #2228. */ - switch_mode("default"); - - /* First ungrab the keys */ - ungrab_all_keys(conn); - - struct Mode *mode; - while (!SLIST_EMPTY(&modes)) { - mode = SLIST_FIRST(&modes); - FREE(mode->name); - - /* Clear the old binding list */ - while (!TAILQ_EMPTY(mode->bindings)) { - Binding *bind = TAILQ_FIRST(mode->bindings); - TAILQ_REMOVE(mode->bindings, bind, bindings); - binding_free(bind); - } - FREE(mode->bindings); - - SLIST_REMOVE(&modes, mode, Mode, modes); - FREE(mode); - } - - while (!TAILQ_EMPTY(&assignments)) { - struct Assignment *assign = TAILQ_FIRST(&assignments); - if (assign->type == A_TO_WORKSPACE || assign->type == A_TO_WORKSPACE_NUMBER) - FREE(assign->dest.workspace); - else if (assign->type == A_COMMAND) - FREE(assign->dest.command); - else if (assign->type == A_TO_OUTPUT) - FREE(assign->dest.output); - match_free(&(assign->match)); - TAILQ_REMOVE(&assignments, assign, assignments); - FREE(assign); - } - - while (!TAILQ_EMPTY(&ws_assignments)) { - struct Workspace_Assignment *assign = TAILQ_FIRST(&ws_assignments); - FREE(assign->name); - FREE(assign->output); - TAILQ_REMOVE(&ws_assignments, assign, ws_assignments); - FREE(assign); - } - - /* Clear bar configs */ - Barconfig *barconfig; - while (!TAILQ_EMPTY(&barconfigs)) { - barconfig = TAILQ_FIRST(&barconfigs); - FREE(barconfig->id); - for (int c = 0; c < barconfig->num_outputs; c++) - free(barconfig->outputs[c]); - - while (!TAILQ_EMPTY(&(barconfig->bar_bindings))) { - struct Barbinding *binding = TAILQ_FIRST(&(barconfig->bar_bindings)); - FREE(binding->command); - TAILQ_REMOVE(&(barconfig->bar_bindings), binding, bindings); - FREE(binding); - } - - while (!TAILQ_EMPTY(&(barconfig->tray_outputs))) { - struct tray_output_t *tray_output = TAILQ_FIRST(&(barconfig->tray_outputs)); - FREE(tray_output->output); - TAILQ_REMOVE(&(barconfig->tray_outputs), tray_output, tray_outputs); - FREE(tray_output); - } - - FREE(barconfig->outputs); - FREE(barconfig->socket_path); - FREE(barconfig->status_command); - FREE(barconfig->i3bar_command); - FREE(barconfig->font); - FREE(barconfig->colors.background); - FREE(barconfig->colors.statusline); - FREE(barconfig->colors.separator); - FREE(barconfig->colors.focused_background); - FREE(barconfig->colors.focused_statusline); - FREE(barconfig->colors.focused_separator); - FREE(barconfig->colors.focused_workspace_border); - FREE(barconfig->colors.focused_workspace_bg); - FREE(barconfig->colors.focused_workspace_text); - FREE(barconfig->colors.active_workspace_border); - FREE(barconfig->colors.active_workspace_bg); - FREE(barconfig->colors.active_workspace_text); - FREE(barconfig->colors.inactive_workspace_border); - FREE(barconfig->colors.inactive_workspace_bg); - FREE(barconfig->colors.inactive_workspace_text); - FREE(barconfig->colors.urgent_workspace_border); - FREE(barconfig->colors.urgent_workspace_bg); - FREE(barconfig->colors.urgent_workspace_text); - FREE(barconfig->colors.binding_mode_border); - FREE(barconfig->colors.binding_mode_bg); - FREE(barconfig->colors.binding_mode_text); - TAILQ_REMOVE(&barconfigs, barconfig, configs); - FREE(barconfig); - } - - Con *con; - TAILQ_FOREACH(con, &all_cons, all_cons) { - /* Assignments changed, previously ran assignments are invalid. */ - if (con->window) { - con->window->nr_assignments = 0; - FREE(con->window->ran_assignments); - } - /* Invalidate pixmap caches in case font or colors changed. */ - FREE(con->deco_render_params); - } - - /* Get rid of the current font */ - free_font(); - - free(config.ipc_socket_path); - free(config.restart_state_path); - free(config.fake_outputs); + free_configuration(); } SLIST_INIT(&modes); From 08cdc3a6ae56623c7604af690c3fd2ef238d9ffa Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Fri, 29 Mar 2019 02:36:24 +0200 Subject: [PATCH 75/98] Allow checking for duplicate bindings with -C - Having both parse_configuration and parse_file is excessive now - We detect if we are parsing only by checking if conn is NULL, not with use_nagbar - font.pattern needs to be set to NULL because it is freed in free_font() Fixes #3660 --- include/configuration.h | 24 +++++------ libi3/font.c | 1 + src/commands.c | 2 +- src/config.c | 64 +++++++++++------------------ src/main.c | 4 +- testcases/t/235-check-config-no-x.t | 29 +++++++++++++ 6 files changed, 68 insertions(+), 56 deletions(-) diff --git a/include/configuration.h b/include/configuration.h index 1971146b..872f11c8 100644 --- a/include/configuration.h +++ b/include/configuration.h @@ -400,19 +400,11 @@ struct tray_output_t { tray_outputs; }; -/** - * Finds the configuration file to use (either the one specified by - * override_configpath), the user’s one or the system default) and calls - * parse_file(). - * - * If you specify override_configpath, only this path is used to look for a - * configuration file. - * - * If use_nagbar is false, don't try to start i3-nagbar but log the errors to - * stdout/stderr instead. - * - */ -bool parse_configuration(const char *override_configpath, bool use_nagbar); +typedef enum { + C_VALIDATE, + C_LOAD, + C_RELOAD, +} config_load_t; /** * (Re-)loads the configuration file (sets useful defaults before). @@ -420,8 +412,12 @@ bool parse_configuration(const char *override_configpath, bool use_nagbar); * If you specify override_configpath, only this path is used to look for a * configuration file. * + * load_type specifies the type of loading: C_VALIDATE is used to only verify + * the correctness of the config file (used with the flag -C). C_LOAD will load + * the config for normal use and display errors in the nagbar. C_RELOAD will + * also clear the previous config. */ -void load_configuration(const char *override_configfile, bool reload); +bool load_configuration(const char *override_configfile, config_load_t load_type); /** * Ungrabs all keys, to be called before re-grabbing the keys because of a diff --git a/libi3/font.c b/libi3/font.c index c06bae00..e16ce85e 100644 --- a/libi3/font.c +++ b/libi3/font.c @@ -163,6 +163,7 @@ i3Font load_font(const char *pattern, const bool fallback) { i3Font font; font.type = FONT_TYPE_NONE; + font.pattern = NULL; /* No XCB connction, return early because we're just validating the * configuration file. */ diff --git a/src/commands.c b/src/commands.c index 69015d92..624c27db 100644 --- a/src/commands.c +++ b/src/commands.c @@ -1573,7 +1573,7 @@ void cmd_reload(I3_CMD) { LOG("reloading\n"); kill_nagbar(&config_error_nagbar_pid, false); kill_nagbar(&command_error_nagbar_pid, false); - load_configuration(NULL, true); + load_configuration(NULL, C_RELOAD); x_set_i3_atoms(); /* Send an IPC event just in case the ws names have changed */ ipc_send_workspace_event("reload", NULL, NULL); diff --git a/src/config.c b/src/config.c index e0eb5378..81919566 100644 --- a/src/config.c +++ b/src/config.c @@ -159,42 +159,20 @@ static void free_configuration(void) { free(config.fake_outputs); } -/* - * Finds the configuration file to use (either the one specified by - * override_configpath), the user’s one or the system default) and calls - * parse_file(). - * - */ -bool parse_configuration(const char *override_configpath, bool use_nagbar) { - char *path = get_config_path(override_configpath, true); - if (path == NULL) { - die("Unable to find the configuration file (looked at " - "$XDG_CONFIG_HOME/i3/config, ~/.i3/config, $XDG_CONFIG_DIRS/i3/config " - "and " SYSCONFDIR "/i3/config)"); - } - - LOG("Parsing configfile %s\n", path); - FREE(current_configpath); - current_configpath = path; - - /* initialize default bindings if we're just validating the config file */ - if (!use_nagbar && bindings == NULL) { - bindings = scalloc(1, sizeof(struct bindings_head)); - TAILQ_INIT(bindings); - } - - return parse_file(path, use_nagbar); -} - /* * (Re-)loads the configuration file (sets useful defaults before). * * If you specify override_configpath, only this path is used to look for a * configuration file. * + * load_type specifies the type of loading: C_VALIDATE is used to only verify + * the correctness of the config file (used with the flag -C). C_LOAD will load + * the config for normal use and display errors in the nagbar. C_RELOAD will + * also clear the previous config. + * */ -void load_configuration(const char *override_configpath, bool reload) { - if (reload) { +bool load_configuration(const char *override_configpath, config_load_t load_type) { + if (load_type == C_RELOAD) { free_configuration(); } @@ -250,24 +228,32 @@ void load_configuration(const char *override_configpath, bool reload) { config.focus_wrapping = FOCUS_WRAPPING_ON; - parse_configuration(override_configpath, true); - - if (reload) { - translate_keysyms(); - grab_all_keys(conn); - regrab_all_buttons(conn); + FREE(current_configpath); + current_configpath = get_config_path(override_configpath, true); + if (current_configpath == NULL) { + die("Unable to find the configuration file (looked at " + "$XDG_CONFIG_HOME/i3/config, ~/.i3/config, $XDG_CONFIG_DIRS/i3/config " + "and " SYSCONFDIR "/i3/config)"); } + LOG("Parsing configfile %s\n", current_configpath); + const bool result = parse_file(current_configpath, load_type != C_VALIDATE); - if (config.font.type == FONT_TYPE_NONE) { + if (config.font.type == FONT_TYPE_NONE && load_type != C_VALIDATE) { ELOG("You did not specify required configuration option \"font\"\n"); config.font = load_font("fixed", true); set_font(&config.font); } - /* Redraw the currently visible decorations on reload, so that - * the possibly new drawing parameters changed. */ - if (reload) { + if (load_type == C_RELOAD) { + translate_keysyms(); + grab_all_keys(conn); + regrab_all_buttons(conn); + + /* Redraw the currently visible decorations on reload, so that the + * possibly new drawing parameters changed. */ x_deco_recurse(croot); xcb_flush(conn); } + + return result; } diff --git a/src/main.c b/src/main.c index d7d7530a..3ebdbf0c 100644 --- a/src/main.c +++ b/src/main.c @@ -420,7 +420,7 @@ int main(int argc, char *argv[]) { } if (only_check_config) { - exit(parse_configuration(override_configpath, false) ? 0 : 1); + exit(load_configuration(override_configpath, C_VALIDATE) ? 0 : 1); } /* If the user passes more arguments, we act like i3-msg would: Just send @@ -586,7 +586,7 @@ int main(int argc, char *argv[]) { #include "atoms.xmacro" #undef xmacro - load_configuration(override_configpath, false); + load_configuration(override_configpath, C_LOAD); if (config.ipc_socket_path == NULL) { /* Fall back to a file name in /tmp/ based on the PID */ diff --git a/testcases/t/235-check-config-no-x.t b/testcases/t/235-check-config-no-x.t index ef621142..dce70894 100644 --- a/testcases/t/235-check-config-no-x.t +++ b/testcases/t/235-check-config-no-x.t @@ -59,4 +59,33 @@ EOT is($ret, 0, "exit code == 0"); is($out, "", 'valid config file'); +################################################################################ +# 3: test duplicate keybindings +################################################################################ + +$cfg = < Date: Fri, 29 Mar 2019 16:10:19 +0200 Subject: [PATCH 76/98] child_handle_button: Call only if x >= offset c2a1b6e9 was a bit overzealous, other actions should be executed if the button was pressed after the workspace buttons but before the statusline. Also, should we allow other actions everywhere in the bar if click events are disabled by the child? --- i3bar/src/xcb.c | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/i3bar/src/xcb.c b/i3bar/src/xcb.c index fb331a08..b6f27738 100644 --- a/i3bar/src/xcb.c +++ b/i3bar/src/xcb.c @@ -454,19 +454,12 @@ static bool execute_custom_command(xcb_keycode_t input_code, bool event_is_relea return false; } -static void child_handle_button(xcb_button_press_event_t *event, i3_output *output, int32_t x) { +static void child_handle_button(xcb_button_press_event_t *event, i3_output *output, uint32_t statusline_x) { if (!child_want_click_events()) { return; } - const int tray_width = get_tray_width(output->trayclients); - /* Calculate the horizontal coordinate (x) of the start of the statusline by - * subtracting its width and the width of the tray from the bar width. */ - const int offset = output->rect.w - output->statusline_width - tray_width - logical_px((tray_width > 0) * sb_hoff_px); - /* x of the click event relative to the start of the statusline. */ - const uint32_t statusline_x = x - offset; - - if (x < offset || statusline_x > (uint32_t)output->statusline_width) { + if (statusline_x > (uint32_t)output->statusline_width) { return; } @@ -551,12 +544,26 @@ static void handle_button(xcb_button_press_event_t *event) { } if (x > workspace_width) { - if (!event_is_release) { - child_handle_button(event, walk, x); + const int tray_width = get_tray_width(walk->trayclients); + /* Calculate the horizontal coordinate (x) of the start of the + * statusline by subtracting its width and the width of the tray from + * the bar width. */ + const int offset = walk->rect.w - walk->statusline_width - + tray_width - logical_px((tray_width > 0) * sb_hoff_px); + if (x >= offset) { + /* Click was after the start of the statusline, return to avoid + * executing any other actions even if a click event is not + * produced eventually. */ + + if (!event_is_release) { + /* x of the click event relative to the start of the + * statusline. */ + const uint32_t statusline_x = x - offset; + child_handle_button(event, walk, statusline_x); + } + + return; } - /* Return to avoid executing any other actions when a separator is - * clicked. */ - return; } /* If a custom command was specified for this mouse button, it overrides From ea6068a02d54c7f9995c94d89c53078bb0097906 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Sat, 30 Mar 2019 13:20:32 +0200 Subject: [PATCH 77/98] Replace scalloc + strncpy with sstrndup --- i3-msg/main.c | 13 +++++-------- src/ipc.c | 5 ++--- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/i3-msg/main.c b/i3-msg/main.c index 0ada5f64..3a897416 100644 --- a/i3-msg/main.c +++ b/i3-msg/main.c @@ -77,8 +77,8 @@ static int reply_boolean_cb(void *params, int val) { } static int reply_string_cb(void *params, const unsigned char *val, size_t len) { - char *str = scalloc(len + 1, 1); - strncpy(str, (const char *)val, len); + char *str = sstrndup((const char *)val, len); + if (strcmp(last_key, "error") == 0) last_reply.error = str; else if (strcmp(last_key, "input") == 0) @@ -108,8 +108,7 @@ static int reply_end_map_cb(void *params) { static int reply_map_key_cb(void *params, const unsigned char *keyVal, size_t keyLen) { free(last_key); - last_key = scalloc(keyLen + 1, 1); - strncpy(last_key, (const char *)keyVal, keyLen); + last_key = sstrndup((const char *)keyVal, keyLen); return 1; } @@ -128,8 +127,7 @@ static yajl_callbacks reply_callbacks = { static char *config_last_key = NULL; static int config_string_cb(void *params, const unsigned char *val, size_t len) { - char *str = scalloc(len + 1, 1); - strncpy(str, (const char *)val, len); + char *str = sstrndup((const char *)val, len); if (strcmp(config_last_key, "config") == 0) { fprintf(stdout, "%s", str); } @@ -146,8 +144,7 @@ static int config_end_map_cb(void *params) { } static int config_map_key_cb(void *params, const unsigned char *keyVal, size_t keyLen) { - config_last_key = scalloc(keyLen + 1, 1); - strncpy(config_last_key, (const char *)keyVal, keyLen); + config_last_key = sstrndup((const char *)keyVal, keyLen); return 1; } diff --git a/src/ipc.c b/src/ipc.c index 2432d7a5..e548b5a6 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -215,12 +215,11 @@ void ipc_shutdown(shutdown_reason_t reason) { IPC_HANDLER(run_command) { /* To get a properly terminated buffer, we copy * message_size bytes out of the buffer */ - char *command = scalloc(message_size + 1, 1); - strncpy(command, (const char *)message, message_size); + char *command = sstrndup((const char *)message, message_size); LOG("IPC: received: *%s*\n", command); yajl_gen gen = yajl_gen_alloc(NULL); - CommandResult *result = parse_command((const char *)command, gen); + CommandResult *result = parse_command(command, gen); free(command); if (result->needs_tree_render) From 8b88f00117c6c251efd720aedb97afd55fdf76f6 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Sat, 30 Mar 2019 13:31:59 +0200 Subject: [PATCH 78/98] window.c: Reduce code in window_update_* functions --- src/window.c | 40 ++++++++++++---------------------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/src/window.c b/src/window.c index 799488c6..8c3ae850 100644 --- a/src/window.c +++ b/src/window.c @@ -51,14 +51,10 @@ void window_update_class(i3Window *win, xcb_get_property_reply_t *prop, bool bef LOG("WM_CLASS changed to %s (instance), %s (class)\n", win->class_instance, win->class_class); - if (before_mgmt) { - free(prop); - return; - } - - run_assignments(win); - free(prop); + if (!before_mgmt) { + run_assignments(win); + } } /* @@ -92,14 +88,10 @@ void window_update_name(i3Window *win, xcb_get_property_reply_t *prop, bool befo win->uses_net_wm_name = true; - if (before_mgmt) { - free(prop); - return; - } - - run_assignments(win); - free(prop); + if (!before_mgmt) { + run_assignments(win); + } } /* @@ -141,14 +133,10 @@ void window_update_name_legacy(i3Window *win, xcb_get_property_reply_t *prop, bo win->name_x_changed = true; - if (before_mgmt) { - free(prop); - return; - } - - run_assignments(win); - free(prop); + if (!before_mgmt) { + run_assignments(win); + } } /* @@ -244,14 +232,10 @@ void window_update_role(i3Window *win, xcb_get_property_reply_t *prop, bool befo win->role = new_role; LOG("WM_WINDOW_ROLE changed to \"%s\"\n", win->role); - if (before_mgmt) { - free(prop); - return; - } - - run_assignments(win); - free(prop); + if (!before_mgmt) { + run_assignments(win); + } } /* From cc9b227978d06777034f95643aa643c0dee079e4 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Sat, 30 Mar 2019 13:43:36 +0200 Subject: [PATCH 79/98] Make small DLOG improvements - manage_window: log the window in the start of the function so that the reader knows what the rest of the messages refer to, even if the function exits prematurely. - con_is_floating: Message is spammy --- src/con.c | 1 - src/manage.c | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/con.c b/src/con.c index a88909e1..4b640eb8 100644 --- a/src/con.c +++ b/src/con.c @@ -539,7 +539,6 @@ bool con_is_internal(Con *con) { */ bool con_is_floating(Con *con) { assert(con != NULL); - DLOG("checking if con %p is floating\n", con); return (con->floating >= FLOATING_AUTO_ON); } diff --git a/src/manage.c b/src/manage.c index c1468123..80faa167 100644 --- a/src/manage.c +++ b/src/manage.c @@ -80,6 +80,8 @@ void restore_geometry(void) { */ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cookie, bool needs_to_be_mapped) { + DLOG("window 0x%08x\n", window); + xcb_drawable_t d = {window}; xcb_get_geometry_cookie_t geomc; xcb_get_geometry_reply_t *geom; @@ -163,8 +165,6 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki wm_user_time_cookie = GET_PROPERTY(A__NET_WM_USER_TIME, UINT32_MAX); wm_desktop_cookie = GET_PROPERTY(A__NET_WM_DESKTOP, UINT32_MAX); - DLOG("Managing window 0x%08x\n", window); - i3Window *cwindow = scalloc(1, sizeof(i3Window)); cwindow->id = window; cwindow->depth = get_visual_depth(attr->visual); From 58c808a961abd6408d37740138e3043eb8438d99 Mon Sep 17 00:00:00 2001 From: Jeremy Klotz Date: Wed, 10 Apr 2019 19:56:30 -0400 Subject: [PATCH 80/98] Fix memory leak --- libi3/font.c | 1 + 1 file changed, 1 insertion(+) diff --git a/libi3/font.c b/libi3/font.c index e16ce85e..32744c0b 100644 --- a/libi3/font.c +++ b/libi3/font.c @@ -436,6 +436,7 @@ static int xcb_query_text_width(const xcb_char2b_t *text, size_t text_len) { * a crash. Plus, the user will see the error in their log. */ fprintf(stderr, "Could not get text extents (X error code %d)\n", error->error_code); + free(error); return savedFont->specific.xcb.info->max_bounds.character_width * text_len; } From 4a37d2060248a148293143f46aef0febcf3efa9b Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Fri, 3 May 2019 15:38:37 +0300 Subject: [PATCH 81/98] ewmh: Introduce FOREACH_NONINTERNAL macro --- src/ewmh.c | 102 ++++++++++++++++++----------------------------------- 1 file changed, 34 insertions(+), 68 deletions(-) diff --git a/src/ewmh.c b/src/ewmh.c index e5dcafcb..bd8100b3 100644 --- a/src/ewmh.c +++ b/src/ewmh.c @@ -11,6 +11,11 @@ xcb_window_t ewmh_window; +#define FOREACH_NONINTERNAL \ + TAILQ_FOREACH(output, &(croot->nodes_head), nodes) \ + TAILQ_FOREACH(ws, &(output_get_content(output)->nodes_head), nodes) \ + if (!con_is_internal(ws)) + /* * Updates _NET_CURRENT_DESKTOP with the current desktop number. * @@ -30,17 +35,12 @@ void ewmh_update_current_desktop(void) { * noninternal workspaces. */ void ewmh_update_number_of_desktops(void) { - Con *output; + Con *output, *ws; uint32_t idx = 0; - TAILQ_FOREACH(output, &(croot->nodes_head), nodes) { - Con *ws; - TAILQ_FOREACH(ws, &(output_get_content(output)->nodes_head), nodes) { - if (STARTS_WITH(ws->name, "__")) - continue; - ++idx; - } - } + FOREACH_NONINTERNAL { + idx++; + }; xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A__NET_NUMBER_OF_DESKTOPS, XCB_ATOM_CARDINAL, 32, 1, &idx); @@ -51,32 +51,21 @@ void ewmh_update_number_of_desktops(void) { * list of NULL-terminated strings in UTF-8 encoding" */ void ewmh_update_desktop_names(void) { - Con *output; + Con *output, *ws; int msg_length = 0; /* count the size of the property message to set */ - TAILQ_FOREACH(output, &(croot->nodes_head), nodes) { - Con *ws; - TAILQ_FOREACH(ws, &(output_get_content(output)->nodes_head), nodes) { - if (STARTS_WITH(ws->name, "__")) - continue; - msg_length += strlen(ws->name) + 1; - } - } + FOREACH_NONINTERNAL { + msg_length += strlen(ws->name) + 1; + }; char desktop_names[msg_length]; int current_position = 0; /* fill the buffer with the names of the i3 workspaces */ - TAILQ_FOREACH(output, &(croot->nodes_head), nodes) { - Con *ws; - TAILQ_FOREACH(ws, &(output_get_content(output)->nodes_head), nodes) { - if (STARTS_WITH(ws->name, "__")) - continue; - - for (size_t i = 0; i < strlen(ws->name) + 1; i++) { - desktop_names[current_position++] = ws->name[i]; - } + FOREACH_NONINTERNAL { + for (size_t i = 0; i < strlen(ws->name) + 1; i++) { + desktop_names[current_position++] = ws->name[i]; } } @@ -89,32 +78,20 @@ void ewmh_update_desktop_names(void) { * define the top left corner of each desktop's viewport. */ void ewmh_update_desktop_viewport(void) { - Con *output; + Con *output, *ws; int num_desktops = 0; /* count number of desktops */ - TAILQ_FOREACH(output, &(croot->nodes_head), nodes) { - Con *ws; - TAILQ_FOREACH(ws, &(output_get_content(output)->nodes_head), nodes) { - if (STARTS_WITH(ws->name, "__")) - continue; - - num_desktops++; - } + FOREACH_NONINTERNAL { + num_desktops++; } uint32_t viewports[num_desktops * 2]; int current_position = 0; /* fill the viewport buffer */ - TAILQ_FOREACH(output, &(croot->nodes_head), nodes) { - Con *ws; - TAILQ_FOREACH(ws, &(output_get_content(output)->nodes_head), nodes) { - if (STARTS_WITH(ws->name, "__")) - continue; - - viewports[current_position++] = output->rect.x; - viewports[current_position++] = output->rect.y; - } + FOREACH_NONINTERNAL { + viewports[current_position++] = output->rect.x; + viewports[current_position++] = output->rect.y; } xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, @@ -354,18 +331,12 @@ Con *ewmh_get_workspace_by_index(uint32_t idx) { uint32_t current_index = 0; - Con *output; - TAILQ_FOREACH(output, &(croot->nodes_head), nodes) { - Con *workspace; - TAILQ_FOREACH(workspace, &(output_get_content(output)->nodes_head), nodes) { - if (con_is_internal(workspace)) - continue; - - if (current_index == idx) - return workspace; - - ++current_index; + Con *output, *ws; + FOREACH_NONINTERNAL { + if (current_index == idx) { + return ws; } + current_index++; } return NULL; @@ -381,19 +352,14 @@ Con *ewmh_get_workspace_by_index(uint32_t idx) { uint32_t ewmh_get_workspace_index(Con *con) { uint32_t index = 0; - Con *workspace = con_get_workspace(con); - Con *output; - TAILQ_FOREACH(output, &(croot->nodes_head), nodes) { - Con *current; - TAILQ_FOREACH(current, &(output_get_content(output)->nodes_head), nodes) { - if (con_is_internal(current)) - continue; - - if (current == workspace) - return index; - - ++index; + Con *target_workspace = con_get_workspace(con); + Con *output, *ws; + FOREACH_NONINTERNAL { + if (ws == target_workspace) { + return index; } + + index++; } return NET_WM_DESKTOP_NONE; From 8c25bc1bd453535a21fb02e4cf5236fe4e3a7062 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Fri, 3 May 2019 15:39:04 +0300 Subject: [PATCH 82/98] tree_close_internal: Log workspace name in EWMH message --- src/tree.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tree.c b/src/tree.c index 5023e894..f93b2262 100644 --- a/src/tree.c +++ b/src/tree.c @@ -300,7 +300,7 @@ bool tree_close_internal(Con *con, kill_window_t kill_window, bool dont_kill_par x_con_kill(con); if (ws == con) { - DLOG("Closing a workspace container, updating EWMH atoms\n"); + DLOG("Closing workspace container %s, updating EWMH atoms\n", ws->name); ewmh_update_number_of_desktops(); ewmh_update_desktop_names(); ewmh_update_wm_desktop(); From 830465b39f60bd22bbb5053a1e664d30e03351ad Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Fri, 3 May 2019 16:04:32 +0300 Subject: [PATCH 83/98] ewmh: Cache idx to avoid xcb_change_property calls Updates ewmh_update_current_desktop, ewmh_update_number_of_desktops --- src/ewmh.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/ewmh.c b/src/ewmh.c index bd8100b3..a26eef39 100644 --- a/src/ewmh.c +++ b/src/ewmh.c @@ -24,10 +24,15 @@ xcb_window_t ewmh_window; * */ void ewmh_update_current_desktop(void) { + static uint32_t old_idx = NET_WM_DESKTOP_NONE; const uint32_t idx = ewmh_get_workspace_index(focused); - if (idx != NET_WM_DESKTOP_NONE) { - xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A__NET_CURRENT_DESKTOP, XCB_ATOM_CARDINAL, 32, 1, &idx); + + if (idx == old_idx || idx == NET_WM_DESKTOP_NONE) { + return; } + old_idx = idx; + + xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A__NET_CURRENT_DESKTOP, XCB_ATOM_CARDINAL, 32, 1, &idx); } /* @@ -36,12 +41,18 @@ void ewmh_update_current_desktop(void) { */ void ewmh_update_number_of_desktops(void) { Con *output, *ws; + static uint32_t old_idx = 0; uint32_t idx = 0; FOREACH_NONINTERNAL { idx++; }; + if (idx == old_idx) { + return; + } + old_idx = idx; + xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A__NET_NUMBER_OF_DESKTOPS, XCB_ATOM_CARDINAL, 32, 1, &idx); } From c9efa6dffe1dfda263d5ea25079168945ec52510 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Fri, 3 May 2019 15:46:38 +0300 Subject: [PATCH 84/98] Call all ewmh_update_* functions together when necessary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The testcase is changed because it was actually incorrect. Easy to verify because: > _NET_CURRENT_DESKTOP > … > The index of the current desktop. This is always an integer between 0 > and _NET_NUMBER_OF_DESKTOPS - 1. Fixes #3696. Also updates the viewports. Finally, fixes an issue with _NET_CURRENT_DESKTOP not being updated after a workspace rename. Example: - workspaces 1, 2, 3 - rename workspace 1 to 5 - All workspaces changed their index but _NET_CURRENT_DESKTOP was not updated --- include/ewmh.h | 24 ++++++------------------ src/commands.c | 4 +--- src/ewmh.c | 18 +++++++++++++++--- src/main.c | 5 +---- src/tree.c | 4 +--- src/workspace.c | 10 ++-------- testcases/t/294-update-ewmh-atoms.t | 10 +++++++++- 7 files changed, 35 insertions(+), 40 deletions(-) diff --git a/include/ewmh.h b/include/ewmh.h index 01ae67f9..f616eb84 100644 --- a/include/ewmh.h +++ b/include/ewmh.h @@ -11,6 +11,12 @@ #include +/** + * Updates all the EWMH desktop properties. + * + */ +void ewmh_update_desktop_properties(void); + /** * Updates _NET_CURRENT_DESKTOP with the current desktop number. * @@ -20,24 +26,6 @@ */ void ewmh_update_current_desktop(void); -/** - * Updates _NET_NUMBER_OF_DESKTOPS which we interpret as the number of - * noninternal workspaces. - */ -void ewmh_update_number_of_desktops(void); - -/** - * Updates _NET_DESKTOP_NAMES: "The names of all virtual desktops. This is a - * list of NULL-terminated strings in UTF-8 encoding" - */ -void ewmh_update_desktop_names(void); - -/** - * Updates _NET_DESKTOP_VIEWPORT, which is an array of pairs of cardinals that - * define the top left corner of each desktop's viewport. - */ -void ewmh_update_desktop_viewport(void); - /** * Updates _NET_WM_DESKTOP for all windows. * A request will only be made if the cached value differs from the calculated value. diff --git a/src/commands.c b/src/commands.c index 624c27db..db0b075d 100644 --- a/src/commands.c +++ b/src/commands.c @@ -2002,9 +2002,7 @@ void cmd_rename_workspace(I3_CMD, const char *old_name, const char *new_name) { cmd_output->needs_tree_render = true; ysuccess(true); - ewmh_update_desktop_names(); - ewmh_update_desktop_viewport(); - ewmh_update_current_desktop(); + ewmh_update_desktop_properties(); startup_sequence_rename_workspace(old_name_copy, new_name); free(old_name_copy); diff --git a/src/ewmh.c b/src/ewmh.c index a26eef39..7bd23fb7 100644 --- a/src/ewmh.c +++ b/src/ewmh.c @@ -39,7 +39,7 @@ void ewmh_update_current_desktop(void) { * Updates _NET_NUMBER_OF_DESKTOPS which we interpret as the number of * noninternal workspaces. */ -void ewmh_update_number_of_desktops(void) { +static void ewmh_update_number_of_desktops(void) { Con *output, *ws; static uint32_t old_idx = 0; uint32_t idx = 0; @@ -61,7 +61,7 @@ void ewmh_update_number_of_desktops(void) { * Updates _NET_DESKTOP_NAMES: "The names of all virtual desktops. This is a * list of NULL-terminated strings in UTF-8 encoding" */ -void ewmh_update_desktop_names(void) { +static void ewmh_update_desktop_names(void) { Con *output, *ws; int msg_length = 0; @@ -88,7 +88,7 @@ void ewmh_update_desktop_names(void) { * Updates _NET_DESKTOP_VIEWPORT, which is an array of pairs of cardinals that * define the top left corner of each desktop's viewport. */ -void ewmh_update_desktop_viewport(void) { +static void ewmh_update_desktop_viewport(void) { Con *output, *ws; int num_desktops = 0; /* count number of desktops */ @@ -109,6 +109,18 @@ void ewmh_update_desktop_viewport(void) { A__NET_DESKTOP_VIEWPORT, XCB_ATOM_CARDINAL, 32, current_position, &viewports); } +/* + * Updates all the EWMH desktop properties. + * + */ +void ewmh_update_desktop_properties(void) { + ewmh_update_number_of_desktops(); + ewmh_update_desktop_viewport(); + ewmh_update_current_desktop(); + ewmh_update_desktop_names(); + ewmh_update_wm_desktop(); +} + static void ewmh_update_wm_desktop_recursively(Con *con, const uint32_t desktop) { Con *child; diff --git a/src/main.c b/src/main.c index 3ebdbf0c..9f69834f 100644 --- a/src/main.c +++ b/src/main.c @@ -852,10 +852,7 @@ int main(int argc, char *argv[]) { ewmh_update_workarea(); /* Set the ewmh desktop properties. */ - ewmh_update_current_desktop(); - ewmh_update_number_of_desktops(); - ewmh_update_desktop_names(); - ewmh_update_desktop_viewport(); + ewmh_update_desktop_properties(); struct ev_io *xcb_watcher = scalloc(1, sizeof(struct ev_io)); xcb_prepare = scalloc(1, sizeof(struct ev_prepare)); diff --git a/src/tree.c b/src/tree.c index f93b2262..4057d177 100644 --- a/src/tree.c +++ b/src/tree.c @@ -301,9 +301,7 @@ bool tree_close_internal(Con *con, kill_window_t kill_window, bool dont_kill_par if (ws == con) { DLOG("Closing workspace container %s, updating EWMH atoms\n", ws->name); - ewmh_update_number_of_desktops(); - ewmh_update_desktop_names(); - ewmh_update_wm_desktop(); + ewmh_update_desktop_properties(); } con_free(con); diff --git a/src/workspace.c b/src/workspace.c index 3cf74754..59705798 100644 --- a/src/workspace.c +++ b/src/workspace.c @@ -161,10 +161,7 @@ Con *workspace_get(const char *num, bool *created) { con_attach(workspace, content, false); ipc_send_workspace_event("init", workspace, NULL); - ewmh_update_number_of_desktops(); - ewmh_update_desktop_names(); - ewmh_update_desktop_viewport(); - ewmh_update_wm_desktop(); + ewmh_update_desktop_properties(); if (created != NULL) *created = true; } else if (created != NULL) { @@ -520,10 +517,7 @@ void workspace_show(Con *workspace) { old_focus = NULL; } - ewmh_update_number_of_desktops(); - ewmh_update_desktop_names(); - ewmh_update_desktop_viewport(); - ewmh_update_wm_desktop(); + ewmh_update_desktop_properties(); } } diff --git a/testcases/t/294-update-ewmh-atoms.t b/testcases/t/294-update-ewmh-atoms.t index 047cc119..f13b1764 100644 --- a/testcases/t/294-update-ewmh-atoms.t +++ b/testcases/t/294-update-ewmh-atoms.t @@ -101,11 +101,19 @@ is_deeply(\@actual_names, \@expected_names); # Kill first window to close a workspace. cmd '[id="' . $second->id . '"] kill'; -is(get_current_desktop, 2, '_NET_CURRENT_DESKTOP should be updated'); +is(get_current_desktop, 1, '_NET_CURRENT_DESKTOP should be updated'); is(get_num_of_desktops, 2, '_NET_NUMBER_OF_DESKTOPS should be updated'); my @actual_names = get_desktop_names; my @expected_names = ('0', '2'); is_deeply(\@actual_names, \@expected_names, '_NET_DESKTOP_NAMES should be updated'); +# Rename workspace to reorder them. +cmd 'rename workspace 0 to 5'; + +is(get_current_desktop, 0, '_NET_CURRENT_DESKTOP should be updated'); +is(get_num_of_desktops, 2, '_NET_NUMBER_OF_DESKTOPS should remain the same'); +my @actual_names = get_desktop_names; +my @expected_names = ('2', '5'); +is_deeply(\@actual_names, \@expected_names, '_NET_DESKTOP_NAMES should be updated'); done_testing; From baf1067087c73d2718a6e8b50f7330d063f615e4 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Mon, 13 May 2019 12:56:04 +0300 Subject: [PATCH 85/98] i3bar: If click events disabled, use whole bar for other events Fixes #3700 --- i3bar/src/xcb.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/i3bar/src/xcb.c b/i3bar/src/xcb.c index b6f27738..ccfd5135 100644 --- a/i3bar/src/xcb.c +++ b/i3bar/src/xcb.c @@ -455,10 +455,6 @@ static bool execute_custom_command(xcb_keycode_t input_code, bool event_is_relea } static void child_handle_button(xcb_button_press_event_t *event, i3_output *output, uint32_t statusline_x) { - if (!child_want_click_events()) { - return; - } - if (statusline_x > (uint32_t)output->statusline_width) { return; } @@ -543,7 +539,7 @@ static void handle_button(xcb_button_press_event_t *event) { workspace_width += logical_px(ws_spacing_px); } - if (x > workspace_width) { + if (child_want_click_events() && x > workspace_width) { const int tray_width = get_tray_width(walk->trayclients); /* Calculate the horizontal coordinate (x) of the start of the * statusline by subtracting its width and the width of the tray from From 573574b3011e60b744df934e7a1e2e441f8f0070 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Sat, 25 May 2019 00:05:18 +0300 Subject: [PATCH 86/98] Fix: delete decoration cache after swap Fixes a regression after 8e1687a where swapping 2 containers across different workspaces would not update their titles. --- src/con.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/con.c b/src/con.c index 4b640eb8..10afb4cc 100644 --- a/src/con.c +++ b/src/con.c @@ -2385,6 +2385,8 @@ bool con_swap(Con *first, Con *second) { con_fix_percent(first->parent); con_fix_percent(second->parent); + FREE(first->deco_render_params); + FREE(second->deco_render_params); con_force_split_parents_redraw(first); con_force_split_parents_redraw(second); From e658788895ee7dd6000df2736c7431b4c1190e98 Mon Sep 17 00:00:00 2001 From: Oliver Kraitschy Date: Mon, 3 Jun 2019 07:42:43 +0200 Subject: [PATCH 87/98] docs: describe for_window with the correct term for_window is a directive or option, not a command. --- docs/userguide | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/userguide b/docs/userguide index a6cd29d5..875e886f 100644 --- a/docs/userguide +++ b/docs/userguide @@ -657,7 +657,7 @@ hide_edge_borders vertical [[for_window]] === Arbitrary commands for specific windows (for_window) -With the +for_window+ command, you can let i3 execute any command when it +With the +for_window+ directive, you can let i3 execute any command when it encounters a specific window. This can be used to set windows to floating or to change their border style, for example. From 48af067dfe56545ccf0f462db4f62bb12ac16004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingo=20B=C3=BCrk?= Date: Sat, 22 Jun 2019 17:23:21 +0200 Subject: [PATCH 88/98] feat: support transparency (RGBA) in i3bar (#3727) We introduce a --transparency flag for i3bar in order to enable a mode which supports the use of RGBA colors. An important constraint here is that tray icons will always have a fully transparent background. fixes #3723 --- docs/userguide | 20 ++++++++++++++++++++ i3bar/include/configuration.h | 1 + i3bar/src/main.c | 19 ++++++++++++------- i3bar/src/xcb.c | 16 +++++++++++++++- 4 files changed, 48 insertions(+), 8 deletions(-) diff --git a/docs/userguide b/docs/userguide index 875e886f..0fda7e80 100644 --- a/docs/userguide +++ b/docs/userguide @@ -1744,6 +1744,26 @@ bar { } -------------------------------------- +=== Transparency + +i3bar can support transparency by passing the +--transparency+ flag in the +configuration: + +*Syntax*: +-------------------------------------- +bar { + i3bar_command i3bar --transparency +} +-------------------------------------- + +In the i3bar color configuration and i3bar status block color attribute you can +then use colors in the RGBA format, i.e. the last two (hexadecimal) digits +specify the opacity. For example, +#00000000+ will be completely transparent, +while +#000000FF+ will be a fully opaque black (the same as +#000000+). + +Please note that due to the way the tray specification works, enabling this +flag will cause all tray icons to have a transparent background. + [[list_of_commands]] == List of commands diff --git a/i3bar/include/configuration.h b/i3bar/include/configuration.h index b86da2e0..3d875e5d 100644 --- a/i3bar/include/configuration.h +++ b/i3bar/include/configuration.h @@ -48,6 +48,7 @@ typedef struct config_t { position_t position; bool verbose; + bool transparency; struct xcb_color_strings_t colors; bool disable_binding_mode_indicator; bool disable_ws; diff --git a/i3bar/src/main.c b/i3bar/src/main.c index a818dd97..26ea0eeb 100644 --- a/i3bar/src/main.c +++ b/i3bar/src/main.c @@ -56,13 +56,14 @@ static char *expand_path(char *path) { } static void print_usage(char *elf_name) { - printf("Usage: %s -b bar_id [-s sock_path] [-h] [-v]\n", elf_name); + printf("Usage: %s -b bar_id [-s sock_path] [-t] [-h] [-v]\n", elf_name); printf("\n"); - printf("-b, --bar_id \tBar ID for which to get the configuration\n"); - printf("-s, --socket \tConnect to i3 via \n"); - printf("-h, --help Display this help message and exit\n"); - printf("-v, --version Display version number and exit\n"); - printf("-V, --verbose Enable verbose mode\n"); + printf("-b, --bar_id \tBar ID for which to get the configuration\n"); + printf("-s, --socket \tConnect to i3 via \n"); + printf("-t, --transparency Enable transparency (RGBA colors)\n"); + printf("-h, --help Display this help message and exit\n"); + printf("-v, --version Display version number and exit\n"); + printf("-V, --verbose Enable verbose mode\n"); printf("\n"); printf(" PLEASE NOTE that i3bar will be automatically started by i3\n" " as soon as there is a 'bar' configuration block in your\n" @@ -105,12 +106,13 @@ int main(int argc, char **argv) { static struct option long_opt[] = { {"socket", required_argument, 0, 's'}, {"bar_id", required_argument, 0, 'b'}, + {"transparency", no_argument, 0, 't'}, {"help", no_argument, 0, 'h'}, {"version", no_argument, 0, 'v'}, {"verbose", no_argument, 0, 'V'}, {NULL, 0, 0, 0}}; - while ((opt = getopt_long(argc, argv, "b:s:hvV", long_opt, &option_index)) != -1) { + while ((opt = getopt_long(argc, argv, "b:s:thvV", long_opt, &option_index)) != -1) { switch (opt) { case 's': socket_path = expand_path(optarg); @@ -122,6 +124,9 @@ int main(int argc, char **argv) { case 'b': config.bar_id = sstrdup(optarg); break; + case 't': + config.transparency = true; + break; case 'V': config.verbose = true; break; diff --git a/i3bar/src/xcb.c b/i3bar/src/xcb.c index ccfd5135..33e13c1b 100644 --- a/i3bar/src/xcb.c +++ b/i3bar/src/xcb.c @@ -1197,7 +1197,21 @@ char *init_xcb_early(void) { depth = root_screen->root_depth; colormap = root_screen->default_colormap; - visual_type = get_visualtype(root_screen); + visual_type = config.transparency ? xcb_aux_find_visual_by_attrs(root_screen, -1, 32) : NULL; + if (visual_type != NULL) { + depth = xcb_aux_get_depth_of_visual(root_screen, visual_type->visual_id); + colormap = xcb_generate_id(xcb_connection); + xcb_void_cookie_t cm_cookie = xcb_create_colormap_checked(xcb_connection, + XCB_COLORMAP_ALLOC_NONE, + colormap, + xcb_root, + visual_type->visual_id); + if (xcb_request_failed(cm_cookie, "Could not allocate colormap")) { + exit(EXIT_FAILURE); + } + } else { + visual_type = get_visualtype(root_screen); + } xcb_cursor_context_t *cursor_ctx; if (xcb_cursor_context_new(conn, root_screen, &cursor_ctx) == 0) { From ca82f958125483a0e99c4ab82cda1b188754b32b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingo=20B=C3=BCrk?= Date: Sat, 22 Jun 2019 22:18:29 +0200 Subject: [PATCH 89/98] feat: added support for user-defined border widths in i3bar blocks (#3726) This change introduces support for four new properties on the i3bar protocol, namely "border_top", "border_right", "border_bottom" and "border_left". If a block is drawn with a border, these values define the width of the corresponding edge in pixels. They all default to 1 if not specified to preserve compatibility. fixes #3722 --- docs/i3bar-protocol | 16 ++++++++++++++++ i3bar/include/common.h | 4 ++++ i3bar/src/child.c | 22 ++++++++++++++++++++++ i3bar/src/xcb.c | 17 +++++++++-------- 4 files changed, 51 insertions(+), 8 deletions(-) diff --git a/docs/i3bar-protocol b/docs/i3bar-protocol index 10ee795b..76c51d80 100644 --- a/docs/i3bar-protocol +++ b/docs/i3bar-protocol @@ -141,6 +141,18 @@ background:: Overrides the background color for this particular block. border:: Overrides the border color for this particular block. +border_top:: + Defines the width (in pixels) of the top border of this block. Defaults + to 1. +border_right:: + Defines the width (in pixels) of the right border of this block. Defaults + to 1. +border_bottom:: + Defines the width (in pixels) of the bottom border of this block. Defaults + to 1. +border_left:: + Defines the width (in pixels) of the left border of this block. Defaults + to 1. min_width:: The minimum width (in pixels) of the block. If the content of the +full_text+ key take less space than the specified min_width, the block @@ -215,6 +227,10 @@ An example of a block which uses all possible entries follows: "color": "#00ff00", "background": "#1c1c1c", "border": "#ee0000", + "border_top": 1, + "border_right": 0, + "border_bottom": 3, + "border_left": 1, "min_width": 300, "align": "right", "urgent": false, diff --git a/i3bar/include/common.h b/i3bar/include/common.h index 77be3182..fe7444a3 100644 --- a/i3bar/include/common.h +++ b/i3bar/include/common.h @@ -61,6 +61,10 @@ struct status_block { bool urgent; bool no_separator; + uint32_t border_top; + uint32_t border_right; + uint32_t border_bottom; + uint32_t border_left; bool pango_markup; /* The amount of pixels necessary to render a separater after the block. */ diff --git a/i3bar/src/child.c b/i3bar/src/child.c index 549a2f70..83ee26b4 100644 --- a/i3bar/src/child.c +++ b/i3bar/src/child.c @@ -176,6 +176,12 @@ static int stdin_start_map(void *context) { else ctx->block.sep_block_width = logical_px(8) + separator_symbol_width; + /* By default we draw all four borders if a border is set. */ + ctx->block.border_top = 1; + ctx->block.border_right = 1; + ctx->block.border_bottom = 1; + ctx->block.border_left = 1; + return 1; } @@ -262,6 +268,22 @@ static int stdin_integer(void *context, long long val) { ctx->block.sep_block_width = (uint32_t)val; return 1; } + if (strcasecmp(ctx->last_map_key, "border_top") == 0) { + ctx->block.border_top = (uint32_t)val; + return 1; + } + if (strcasecmp(ctx->last_map_key, "border_right") == 0) { + ctx->block.border_right = (uint32_t)val; + return 1; + } + if (strcasecmp(ctx->last_map_key, "border_bottom") == 0) { + ctx->block.border_bottom = (uint32_t)val; + return 1; + } + if (strcasecmp(ctx->last_map_key, "border_left") == 0) { + ctx->block.border_left = (uint32_t)val; + return 1; + } return 1; } diff --git a/i3bar/src/xcb.c b/i3bar/src/xcb.c index 33e13c1b..412981bc 100644 --- a/i3bar/src/xcb.c +++ b/i3bar/src/xcb.c @@ -213,7 +213,7 @@ static uint32_t predict_statusline_length(bool use_short_text) { render->width = predict_text_width(text); if (block->border) - render->width += logical_px(2); + render->width += logical_px(block->border_left + block->border_right); /* Compute offset and append for text aligment in min_width. */ if (block->min_width <= render->width) { @@ -287,8 +287,8 @@ static void draw_statusline(i3_output *output, uint32_t clip_left, bool use_focu color_t bg_color = bar_color; - int border_width = (block->border) ? logical_px(1) : 0; int full_render_width = render->width + render->x_offset + render->x_append; + int has_border = block->border ? 1 : 0; if (block->border || block->background || block->urgent) { /* Let's determine the colors first. */ color_t border_color = bar_color; @@ -310,15 +310,16 @@ static void draw_statusline(i3_output *output, uint32_t clip_left, bool use_focu /* Draw the background. */ draw_util_rectangle(&output->statusline_buffer, bg_color, - x + border_width, - logical_px(1) + border_width, - full_render_width - 2 * border_width, - bar_height - 2 * border_width - logical_px(2)); + x + has_border * logical_px(block->border_left), + logical_px(1) + has_border * logical_px(block->border_top), + full_render_width - has_border * logical_px(block->border_right + block->border_left), + bar_height - has_border * logical_px(block->border_bottom + block->border_top) - logical_px(2)); } draw_util_text(text, &output->statusline_buffer, fg_color, bg_color, - x + render->x_offset + border_width, logical_px(ws_voff_px), - render->width - 2 * border_width); + x + render->x_offset + has_border * logical_px(block->border_left), + bar_height / 2 - font.height / 2, + render->width - has_border * logical_px(block->border_left + block->border_right)); x += full_render_width; /* If this is not the last block, draw a separator. */ From 48d3d17d479a0bee6b87403404150f8e49eac8d6 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Fri, 19 Jul 2019 20:39:53 +0200 Subject: [PATCH 90/98] strace: switch from deprecated -F to -fvy --- testcases/lib/SocketActivation.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testcases/lib/SocketActivation.pm b/testcases/lib/SocketActivation.pm index 5a5a4484..a2db5453 100644 --- a/testcases/lib/SocketActivation.pm +++ b/testcases/lib/SocketActivation.pm @@ -135,7 +135,7 @@ sub activate_i3 { # We overwrite LISTEN_PID with the correct process ID to make # socket activation work (LISTEN_PID has to match getpid(), # otherwise the LISTEN_FDS will be treated as a left-over). - $cmd = qq|strace -fF -s2048 -v -o "$out" -- | . + $cmd = qq|strace -fvy -s2048 -o "$out" -- | . 'sh -c "export LISTEN_PID=\$\$; ' . $cmd . '"'; } From 865f807976e4c19f5c115ae241cc7792fe925c6b Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Fri, 19 Jul 2019 21:10:40 +0200 Subject: [PATCH 91/98] unflake t/291-swap.t (#3741) --- testcases/t/291-swap.t | 1 + 1 file changed, 1 insertion(+) diff --git a/testcases/t/291-swap.t b/testcases/t/291-swap.t index 8705c1ec..781315e5 100644 --- a/testcases/t/291-swap.t +++ b/testcases/t/291-swap.t @@ -43,6 +43,7 @@ sub fullscreen_windows { sub cmp_floating_rect { my ($window, $rect, $prefix) = @_; + sync_with_i3; my ($absolute, $top) = $window->rect; is($absolute->{width}, $rect->[2], "$prefix: width matches"); From 1ac117bb51765f6b3a681d31bc8a75ccd4e9c83e Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Fri, 19 Jul 2019 21:23:57 +0200 Subject: [PATCH 92/98] docs/ipc: fix code block header/footer mismatches This is required to get the document rendered with the asciidoctor implementation of asciidoc. --- docs/ipc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/ipc b/docs/ipc index f25d7a74..27fa03cb 100644 --- a/docs/ipc +++ b/docs/ipc @@ -80,7 +80,8 @@ Or, as a hexdump: ------------------------------------------------------------------------------ To generate and send such a message, you could use the following code in Perl: ------------------------------------------------------------- + +------------------------------------------------------------------------------- sub format_ipc_command { my ($msg) = @_; my $len; @@ -90,7 +91,7 @@ sub format_ipc_command { } $sock->write(format_ipc_command("exit")); ------------------------------------------------------------------------------- +------------------------------------------------------------------------------- == Receiving replies from i3 @@ -484,7 +485,7 @@ JSON dump: } ] } ------------------------- +----------------------- [[_marks_reply]] === MARKS reply From e4ecc6e4a11a8ea14c9159502c17300fbecb8eb7 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sun, 21 Jul 2019 14:52:12 +0200 Subject: [PATCH 93/98] Make `restart` IPC command send a reply once restart completed (!) (#3743) This is achieved by retaining the IPC connection which is sending the restart command across the restart. This is the cleaner fix for https://github.com/i3/go-i3/issues/3 fixes #3565 --- docs/ipc | 6 +++++ include/commands_parser.h | 6 ++++- include/ipc.h | 22 ++++++++++++++-- src/assignments.c | 2 +- src/bindings.c | 2 +- src/commands.c | 22 +++++++++++++--- src/commands_parser.c | 7 +++-- src/ipc.c | 54 +++++++++++++++++++++++++++++++-------- src/main.c | 25 +++++++++++++++++- src/util.c | 4 +-- 10 files changed, 126 insertions(+), 24 deletions(-) diff --git a/docs/ipc b/docs/ipc index 27fa03cb..847a89e8 100644 --- a/docs/ipc +++ b/docs/ipc @@ -111,6 +111,12 @@ The following reply types are implemented: COMMAND (0):: Confirmation/Error code for the RUN_COMMAND message. ++ +Note that when sending the `restart` command, you will get a reply once +the restart completed. All IPC connection state (e.g. subscriptions) +will reset, and libraries must be able to cope with it. One way of +achieving that is to close the connection, if the library already +supports transparent reconnects. WORKSPACES (1):: Reply to the GET_WORKSPACES message. SUBSCRIBE (2):: diff --git a/include/commands_parser.h b/include/commands_parser.h index b65ae93f..31333af7 100644 --- a/include/commands_parser.h +++ b/include/commands_parser.h @@ -22,6 +22,10 @@ struct CommandResultIR { /* The JSON generator to append a reply to (may be NULL). */ yajl_gen json_gen; + /* The IPC client connection which sent this command (may be NULL, e.g. for + key bindings). */ + ipc_client *client; + /* The next state to transition to. Passed to the function so that we can * determine the next state as a result of a function call, like * cfg_criteria_pop_state() does. */ @@ -61,7 +65,7 @@ char *parse_string(const char **walk, bool as_word); * * Free the returned CommandResult with command_result_free(). */ -CommandResult *parse_command(const char *input, yajl_gen gen); +CommandResult *parse_command(const char *input, yajl_gen gen, ipc_client *client); /** * Frees a CommandResult diff --git a/include/ipc.h b/include/ipc.h index a7743973..a608ef56 100644 --- a/include/ipc.h +++ b/include/ipc.h @@ -72,6 +72,16 @@ typedef void (*handler_t)(ipc_client *, uint8_t *, int, uint32_t, uint32_t); */ void ipc_new_client(EV_P_ struct ev_io *w, int revents); +/** + * ipc_new_client_on_fd() only sets up the event handler + * for activity on the new connection and inserts the file descriptor into + * the list of clients. + * + * This variant is useful for the inherited IPC connection when restarting. + * + */ +ipc_client *ipc_new_client_on_fd(EV_P_ int fd); + /** * Creates the UNIX domain socket at the given path, sets it to non-blocking * mode, bind()s and listen()s on it. @@ -95,10 +105,13 @@ typedef enum { } shutdown_reason_t; /** - * Calls shutdown() on each socket and closes it. + * Calls shutdown() on each socket and closes it. This function is to be called + * when exiting or restarting only! + * + * exempt_fd is never closed. Set to -1 to close all fds. * */ -void ipc_shutdown(shutdown_reason_t reason); +void ipc_shutdown(shutdown_reason_t reason, int exempt_fd); void dump_node(yajl_gen gen, Con *con, bool inplace_restart); @@ -136,3 +149,8 @@ void ipc_send_binding_event(const char *event_type, Binding *bind); * socket. */ void ipc_set_kill_timeout(ev_tstamp new); + +/** + * Sends a restart reply to the IPC client on the specified fd. + */ +void ipc_confirm_restart(ipc_client *client); diff --git a/src/assignments.c b/src/assignments.c index abacc0a3..998ca78a 100644 --- a/src/assignments.c +++ b/src/assignments.c @@ -48,7 +48,7 @@ void run_assignments(i3Window *window) { DLOG("matching assignment, execute command %s\n", current->dest.command); char *full_command; sasprintf(&full_command, "[id=\"%d\"] %s", window->id, current->dest.command); - CommandResult *result = parse_command(full_command, NULL); + CommandResult *result = parse_command(full_command, NULL, NULL); free(full_command); if (result->needs_tree_render) diff --git a/src/bindings.c b/src/bindings.c index 65a1821d..f2921a14 100644 --- a/src/bindings.c +++ b/src/bindings.c @@ -824,7 +824,7 @@ CommandResult *run_binding(Binding *bind, Con *con) { sasprintf(&command, "[con_id=\"%p\"] %s", con, bind->command); Binding *bind_cp = binding_copy(bind); - CommandResult *result = parse_command(command, NULL); + CommandResult *result = parse_command(command, NULL, NULL); free(command); if (result->needs_tree_render) diff --git a/src/commands.c b/src/commands.c index db0b075d..aadf204f 100644 --- a/src/commands.c +++ b/src/commands.c @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include "shmlog.h" @@ -1590,15 +1592,27 @@ void cmd_reload(I3_CMD) { */ void cmd_restart(I3_CMD) { LOG("restarting i3\n"); - ipc_shutdown(SHUTDOWN_REASON_RESTART); + int exempt_fd = -1; + if (cmd_output->client != NULL) { + exempt_fd = cmd_output->client->fd; + LOG("Carrying file descriptor %d across restart\n", exempt_fd); + int flags; + if ((flags = fcntl(exempt_fd, F_GETFD)) < 0 || + fcntl(exempt_fd, F_SETFD, flags & ~FD_CLOEXEC) < 0) { + ELOG("Could not disable FD_CLOEXEC on fd %d\n", exempt_fd); + } + char *fdstr = NULL; + sasprintf(&fdstr, "%d", exempt_fd); + setenv("_I3_RESTART_FD", fdstr, 1); + } + ipc_shutdown(SHUTDOWN_REASON_RESTART, exempt_fd); unlink(config.ipc_socket_path); /* We need to call this manually since atexit handlers don’t get called * when exec()ing */ purge_zerobyte_logfile(); i3_restart(false); - - // XXX: default reply for now, make this a better reply - ysuccess(true); + /* unreached */ + assert(false); } /* diff --git a/src/commands_parser.c b/src/commands_parser.c index 0da65adc..d4ae430b 100644 --- a/src/commands_parser.c +++ b/src/commands_parser.c @@ -181,6 +181,7 @@ static struct CommandResultIR command_output; static void next_state(const cmdp_token *token) { if (token->next_state == __CALL) { subcommand_output.json_gen = command_output.json_gen; + subcommand_output.client = command_output.client; subcommand_output.needs_tree_render = false; GENERATED_call(token->extra.call_identifier, &subcommand_output); state = subcommand_output.next_state; @@ -261,11 +262,13 @@ char *parse_string(const char **walk, bool as_word) { * * Free the returned CommandResult with command_result_free(). */ -CommandResult *parse_command(const char *input, yajl_gen gen) { +CommandResult *parse_command(const char *input, yajl_gen gen, ipc_client *client) { DLOG("COMMAND: *%s*\n", input); state = INITIAL; CommandResult *result = scalloc(1, sizeof(CommandResult)); + command_output.client = client; + /* A YAJL JSON generator used for formatting replies. */ command_output.json_gen = gen; @@ -499,7 +502,7 @@ int main(int argc, char *argv[]) { } yajl_gen gen = yajl_gen_alloc(NULL); - CommandResult *result = parse_command(argv[1], gen); + CommandResult *result = parse_command(argv[1], gen, NULL); command_result_free(result); diff --git a/src/ipc.c b/src/ipc.c index e548b5a6..dc34b7ed 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -33,6 +33,9 @@ all_clients = TAILQ_HEAD_INITIALIZER(all_clients); */ static void set_nonblock(int sockfd) { int flags = fcntl(sockfd, F_GETFL, 0); + if (flags & O_NONBLOCK) { + return; + } flags |= O_NONBLOCK; if (fcntl(sockfd, F_SETFL, flags) < 0) err(-1, "Could not set O_NONBLOCK"); @@ -125,9 +128,11 @@ static void ipc_send_client_message(ipc_client *client, size_t size, const uint3 } } -static void free_ipc_client(ipc_client *client) { - DLOG("Disconnecting client on fd %d\n", client->fd); - close(client->fd); +static void free_ipc_client(ipc_client *client, int exempt_fd) { + if (client->fd != exempt_fd) { + DLOG("Disconnecting client on fd %d\n", client->fd); + close(client->fd); + } ev_io_stop(main_loop, client->read_callback); FREE(client->read_callback); @@ -195,15 +200,19 @@ static void ipc_send_shutdown_event(shutdown_reason_t reason) { * Calls shutdown() on each socket and closes it. This function is to be called * when exiting or restarting only! * + * exempt_fd is never closed. Set to -1 to close all fds. + * */ -void ipc_shutdown(shutdown_reason_t reason) { +void ipc_shutdown(shutdown_reason_t reason, int exempt_fd) { ipc_send_shutdown_event(reason); ipc_client *current; while (!TAILQ_EMPTY(&all_clients)) { current = TAILQ_FIRST(&all_clients); - shutdown(current->fd, SHUT_RDWR); - free_ipc_client(current); + if (current->fd != exempt_fd) { + shutdown(current->fd, SHUT_RDWR); + } + free_ipc_client(current, exempt_fd); } } @@ -219,7 +228,7 @@ IPC_HANDLER(run_command) { LOG("IPC: received: *%s*\n", command); yajl_gen gen = yajl_gen_alloc(NULL); - CommandResult *result = parse_command(command, gen); + CommandResult *result = parse_command(command, gen, client); free(command); if (result->needs_tree_render) @@ -1339,7 +1348,7 @@ static void ipc_receive_message(EV_P_ struct ev_io *w, int revents) { /* If not, there was some kind of error. We don’t bother and close the * connection. Delete the client from the list of clients. */ - free_ipc_client(client); + free_ipc_client(client, -1); FREE(message); return; } @@ -1397,7 +1406,7 @@ end: ELOG("client %p on fd %d timed out, killing\n", client, client->fd); } - free_ipc_client(client); + free_ipc_client(client, -1); } static void ipc_socket_writeable_cb(EV_P_ ev_io *w, int revents) { @@ -1431,6 +1440,18 @@ void ipc_new_client(EV_P_ struct ev_io *w, int revents) { /* Close this file descriptor on exec() */ (void)fcntl(fd, F_SETFD, FD_CLOEXEC); + ipc_new_client_on_fd(EV_A_ fd); +} + +/* + * ipc_new_client_on_fd() only sets up the event handler + * for activity on the new connection and inserts the file descriptor into + * the list of clients. + * + * This variant is useful for the inherited IPC connection when restarting. + * + */ +ipc_client *ipc_new_client_on_fd(EV_P_ int fd) { set_nonblock(fd); ipc_client *client = scalloc(1, sizeof(ipc_client)); @@ -1445,8 +1466,9 @@ void ipc_new_client(EV_P_ struct ev_io *w, int revents) { client->write_callback->data = client; ev_io_init(client->write_callback, ipc_socket_writeable_cb, fd, EV_WRITE); - DLOG("IPC: new client connected on fd %d\n", w->fd); + DLOG("IPC: new client connected on fd %d\n", fd); TAILQ_INSERT_TAIL(&all_clients, client, clients); + return client; } /* @@ -1627,3 +1649,15 @@ void ipc_send_binding_event(const char *event_type, Binding *bind) { y(free); setlocale(LC_NUMERIC, ""); } + +/* + * Sends a restart reply to the IPC client on the specified fd. + */ +void ipc_confirm_restart(ipc_client *client) { + DLOG("ipc_confirm_restart(fd %d)\n", client->fd); + static const char *reply = "{\"success\":true}"; + ipc_send_client_message( + client, strlen(reply), I3_IPC_REPLY_TYPE_COMMAND, + (const uint8_t *)reply); + ipc_push_pending(client); +} diff --git a/src/main.c b/src/main.c index 9f69834f..7ba12bd9 100644 --- a/src/main.c +++ b/src/main.c @@ -166,7 +166,7 @@ static void i3_exit(void) { fflush(stderr); shm_unlink(shmlogname); } - ipc_shutdown(SHUTDOWN_REASON_EXIT); + ipc_shutdown(SHUTDOWN_REASON_EXIT, -1); unlink(config.ipc_socket_path); xcb_disconnect(conn); @@ -236,6 +236,20 @@ static void setup_term_handlers(void) { } } +static int parse_restart_fd(void) { + const char *restart_fd = getenv("_I3_RESTART_FD"); + if (restart_fd == NULL) { + return -1; + } + + long int fd = -1; + if (!parse_long(restart_fd, &fd, 10)) { + ELOG("Malformed _I3_RESTART_FD \"%s\"\n", restart_fd); + return -1; + } + return fd; +} + int main(int argc, char *argv[]) { /* Keep a symbol pointing to the I3_VERSION string constant so that we have * it in gdb backtraces. */ @@ -847,6 +861,15 @@ int main(int argc, char *argv[]) { } } + { + const int restart_fd = parse_restart_fd(); + if (restart_fd != -1) { + DLOG("serving restart fd %d", restart_fd); + ipc_client *client = ipc_new_client_on_fd(main_loop, restart_fd); + ipc_confirm_restart(client); + } + } + /* Set up i3 specific atoms like I3_SOCKET_PATH and I3_CONFIG_PATH */ x_set_i3_atoms(); ewmh_update_workarea(); diff --git a/src/util.c b/src/util.c index 85f359c0..9fe3fa44 100644 --- a/src/util.c +++ b/src/util.c @@ -287,7 +287,7 @@ void i3_restart(bool forget_layout) { restore_geometry(); - ipc_shutdown(SHUTDOWN_REASON_RESTART); + ipc_shutdown(SHUTDOWN_REASON_RESTART, -1); LOG("restarting \"%s\"...\n", start_argv[0]); /* make sure -a is in the argument list or add it */ @@ -465,7 +465,7 @@ void kill_nagbar(pid_t *nagbar_pid, bool wait_for_it) { * if the number could be parsed. */ bool parse_long(const char *str, long *out, int base) { - char *end; + char *end = NULL; long result = strtol(str, &end, base); if (result == LONG_MIN || result == LONG_MAX || result < 0 || (end != NULL && *end != '\0')) { *out = result; From 5bb7b73a4ad28643e8ef82dc739034079b8277cf Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Mon, 29 Jul 2019 13:21:34 -0700 Subject: [PATCH 94/98] restart: make reply an array, add forgotten test to git (#3750) related to #3565 --- src/ipc.c | 2 +- testcases/t/305-restart-reply.t | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 testcases/t/305-restart-reply.t diff --git a/src/ipc.c b/src/ipc.c index dc34b7ed..0ffdfebf 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -1655,7 +1655,7 @@ void ipc_send_binding_event(const char *event_type, Binding *bind) { */ void ipc_confirm_restart(ipc_client *client) { DLOG("ipc_confirm_restart(fd %d)\n", client->fd); - static const char *reply = "{\"success\":true}"; + static const char *reply = "[{\"success\":true}]"; ipc_send_client_message( client, strlen(reply), I3_IPC_REPLY_TYPE_COMMAND, (const uint8_t *)reply); diff --git a/testcases/t/305-restart-reply.t b/testcases/t/305-restart-reply.t new file mode 100644 index 00000000..0493d4ff --- /dev/null +++ b/testcases/t/305-restart-reply.t @@ -0,0 +1,25 @@ +#!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) +# +# Verify that restart produces a reply. +# Ticket: #3565 +# Bug still in: 4.16-143-gca82f958 +use i3test; + +my $reply = cmd('restart'); +ok($reply->[0]->{success}, 'restart reply received'); + +done_testing; From 72975d6764ffd31f810d8a117555785c885a17b1 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sun, 28 Jul 2019 08:55:44 -0700 Subject: [PATCH 95/98] default config: start xss-lock+i3lock, nm-applet, pactl (volume key) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change will make things strictly better for new users (without an existing configuration file) and has no effect on existing users. The tools should be fairly uncontentious, I hope, especially as they only serve as a starting point anyway: users can quickly delete what they don’t want, or change it into what they prefer. But having something is strictly better than having nothing :) We make some space in the config file by removing the old paragraph about pixel fonts, which seems rather outdated and irrelevant to me. --- etc/config | 21 +++++++++++++++------ etc/config.keycodes | 21 +++++++++++++++------ 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/etc/config b/etc/config index f6f2f9db..a0f3b84d 100644 --- a/etc/config +++ b/etc/config @@ -17,12 +17,21 @@ font pango:monospace 8 # text rendering and scalability on retina/hidpi displays (thanks to pango). #font pango:DejaVu Sans Mono 8 -# Before i3 v4.8, we used to recommend this one as the default: -# font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1 -# The font above is very space-efficient, that is, it looks good, sharp and -# clear in small sizes. However, its unicode glyph coverage is limited, the old -# X core fonts rendering does not support right-to-left and this being a bitmap -# font, it doesn't scale on retina/hidpi displays. +# The combination of xss-lock, nm-applet and pactl is a popular choice, so +# they are included here as an example. Modify as you see fit. + +# xss-lock grabs a logind suspend inhibit lock and will use i3lock to lock the +# screen before suspend. +exec --no-startup-id xss-lock --transfer-sleep-lock -- i3lock --nofork + +# NetworkManager is the most popular way to manage wireless networks on Linux, +# and nm-applet is a desktop environment-independent system tray GUI for it. +exec --no-startup-id nm-applet + +# Use pactl to adjust volume in PulseAudio. +bindsym XF86AudioRaiseVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ +10% +bindsym XF86AudioLowerVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ -10% +bindsym XF86AudioMute exec --no-startup-id pactl set-sink-mute @DEFAULT_SINK@ toggle # use these keys for focus, movement, and resize directions when reaching for # the arrows is not convenient diff --git a/etc/config.keycodes b/etc/config.keycodes index 6fc19426..6045ea68 100644 --- a/etc/config.keycodes +++ b/etc/config.keycodes @@ -18,12 +18,21 @@ font pango:monospace 8 # text rendering and scalability on retina/hidpi displays (thanks to pango). #font pango:DejaVu Sans Mono 8 -# Before i3 v4.8, we used to recommend this one as the default: -# font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1 -# The font above is very space-efficient, that is, it looks good, sharp and -# clear in small sizes. However, its unicode glyph coverage is limited, the old -# X core fonts rendering does not support right-to-left and this being a bitmap -# font, it doesn’t scale on retina/hidpi displays. +# The combination of xss-lock, nm-applet and pactl is a popular choice, so +# they are included here as an example. Modify as you see fit. + +# xss-lock grabs a logind suspend inhibit lock and will use i3lock to lock the +# screen before suspend. +exec --no-startup-id xss-lock --transfer-sleep-lock -- i3lock --nofork + +# NetworkManager is the most popular way to manage wireless networks on Linux, +# and nm-applet is a desktop environment-independent system tray GUI for it. +exec --no-startup-id nm-applet + +# Use pactl to adjust volume in PulseAudio. +bindsym XF86AudioRaiseVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ +10% +bindsym XF86AudioLowerVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ -10% +bindsym XF86AudioMute exec --no-startup-id pactl set-sink-mute @DEFAULT_SINK@ toggle # Use Mouse+$mod to drag floating windows to their wanted position floating_modifier $mod From ac100e36d920e4fa4ce4663ae0787f8525b63d57 Mon Sep 17 00:00:00 2001 From: Iskustvo Date: Fri, 2 Aug 2019 23:56:48 +0200 Subject: [PATCH 96/98] Updated the documentation for COMMAND reply. (#3754) --- docs/ipc | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/ipc b/docs/ipc index 847a89e8..ea7a5892 100644 --- a/docs/ipc +++ b/docs/ipc @@ -111,12 +111,6 @@ The following reply types are implemented: COMMAND (0):: Confirmation/Error code for the RUN_COMMAND message. -+ -Note that when sending the `restart` command, you will get a reply once -the restart completed. All IPC connection state (e.g. subscriptions) -will reset, and libraries must be able to cope with it. One way of -achieving that is to close the connection, if the library already -supports transparent reconnects. WORKSPACES (1):: Reply to the GET_WORKSPACES message. SUBSCRIBE (2):: @@ -145,6 +139,20 @@ The reply consists of a list of serialized maps for each command that was parsed. Each has the property +success (bool)+ and may also include a human-readable error message in the property +error (string)+. +NOTE: When sending the `restart` command, you will get a singular reply once the +restart completed. All IPC connection states (e.g. subscriptions) will reset and +all but one socket will be closed. Libraries must be able to cope with this by +aligning their internal states. It is also recommended that libraries close +the last remaining socket(one which replied to `restart` command) to achieve +the full reset. + +NOTE: It is easiest to always send the `restart` command alone: due to i3’s +state reset, the reply messages of preceding commands are lost, and following +commands will not be executed. + +NOTE: When processing the `exit` command, i3 will immediately exit without +sending a reply. Expect the socket to be shut down. + *Example:* ------------------- [{ "success": true }] @@ -777,7 +785,7 @@ This event consists of a single serialized map containing a property +change (string)+ which indicates the type of the change ("focus", "init", "empty", "urgent", "reload", "rename", "restored", "move"). A +current (object)+ property will be present with the affected workspace -whenever the type of event affects a workspace (otherwise, it will be +null). +whenever the type of event affects a workspace (otherwise, it will be +null+). When the change is "focus", an +old (object)+ property will be present with the previous workspace. When the first switch occurs (when i3 focuses the From 79c690248a277383463552c98d6c40728125669a Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sat, 3 Aug 2019 15:14:38 +0200 Subject: [PATCH 97/98] release i3 4.17 --- I3_VERSION | 2 +- Makefile.am | 2 +- RELEASE-NOTES-4.16 | 145 --------------------------------------------- RELEASE-NOTES-4.17 | 96 ++++++++++++++++++++++++++++++ configure.ac | 2 +- 5 files changed, 99 insertions(+), 148 deletions(-) delete mode 100644 RELEASE-NOTES-4.16 create mode 100644 RELEASE-NOTES-4.17 diff --git a/I3_VERSION b/I3_VERSION index a39209b2..82c20487 100644 --- a/I3_VERSION +++ b/I3_VERSION @@ -1 +1 @@ -4.16-non-git +4.17 (2019-08-03) diff --git a/Makefile.am b/Makefile.am index cc7fa845..51c4f539 100644 --- a/Makefile.am +++ b/Makefile.am @@ -118,7 +118,7 @@ EXTRA_DIST = \ I3_VERSION \ LICENSE \ PACKAGE-MAINTAINER \ - RELEASE-NOTES-4.16 \ + RELEASE-NOTES-4.17 \ generate-command-parser.pl \ parser-specs/commands.spec \ parser-specs/config.spec \ diff --git a/RELEASE-NOTES-4.16 b/RELEASE-NOTES-4.16 deleted file mode 100644 index e5505cbf..00000000 --- a/RELEASE-NOTES-4.16 +++ /dev/null @@ -1,145 +0,0 @@ - - ┌────────────────────────────┐ - │ Release notes for i3 v4.16 │ - └────────────────────────────┘ - -This is i3 v4.16. This version is considered stable. All users of i3 are -strongly encouraged to upgrade. - -This release contains a number of assorted fixes and improvements across pretty -much all individual components of i3. - - ┌────────────────────────────┐ - │ Changes in i3 v4.16 │ - └────────────────────────────┘ - - • build: add conditionals for building docs/mans - • docs/i3bar-protocol: mention skipping blocks with empty full_text - • docs/ipc: add window_properties to tree node - • docs/layout-saving: clarify JSON non-compliance - • docs/userguide: clarify X resource value format - • docs/userguide: fix move_to_outputs link - • docs/userguide: link workspace_auto_back_and_forth from workspace - command - • docs/userguide: mention known issues for assign - • docs/userguide: use anchor for list_of_commands - • docs/userguide: add the default keybinding for focus parent - • man/*: fix title markers (for asciidoctor) - • man/i3-msg.man: add get_config and send_tick - • ipc: kill misbehaving subscribed clients instead of hanging - • ipc: introduce the sync IPC command for synchronization with i3bar - • ipc: scratchpad show now returns correct success - • ipc: send_tick now sets the already-documented “first” field - • i3bar-protocol: add modifiers to events sent by i3bar - • dump-asy: use Pod::Usage for --help and perldoc - • dump-asy: introduce -gv flag to disable opening ghostview - • dump-asy: introduce -save flag to store the rendered tree in a file - • dump-asy: add marks - • dump-asy: include floating containers - • i3bar: add --verbose flag - • i3bar: make modifier accept combinations (like floating_modifier) - • i3-config-wizard: add --modifier flag to allow for headless config - • i3-config-wizard: support startup notifications - • i3-msg: only print input + error position if they are set - • i3-msg: check replies also in quiet mode (-q) - • i3-msg: add support for the SUBSCRIBE message type - • i3-nagbar: support startup notifications - • i3-nagbar: add option for button that runs commands without a terminal - • i3-save-tree: exclude unsupported transient_for property - • i3-sensible-terminal: add alacritty - • i3-sensible-terminal: add hyper - • introduce strip_workspace_name alongside strip_workspace_numbers - • introduce title_align config directive - • “border toggle” now accepts an optional pixel argument - • “resize set” now interprets 0 as “no change” - • “resize set” now accepts the “width” and “height” keywords - • “resize” with pixel values now works for tiling containers - • the optional “absolute” method is now silently ignored in “move position” - commands, where it did not cause a visible difference anyway - • the _NET_WM_STATE_FOCUSED atom is now supported, resulting in e.g. - GTK applications displaying the correct window decoration - • moving fullscreen containers now moves them across outputs - • floating windows can now be used with a geometry of e.g. +1+1, i.e. - their top-left corner can be outside any output as long as the window - is contained partially by one - • prefer floating fullscreen containers when switching focus - • moving containers to an active workspace no longer changes focus - • the rename workspace command no longer confuses directions (e.g. “left”) - with output names - • prefer $XDG_CONFIG_HOME/i3/config over ~/.i3/config - • allow multiple assignments of workspaces to output - • respect maximum size in WM_NORMAL_HINTS - • reject requests for WM_STATE_ICONIC, which avoids e.g. wine - applications being stuck in paused state - • a number of code refactorings and cleanups, some of which tool-assisted - - ┌────────────────────────────┐ - │ Bugfixes │ - └────────────────────────────┘ - - • build: fix static linking - • i3bar: fix various memory leaks - • i3bar: fix crash when no status_command is provided - • i3bar: fix chopping the first character on the very left when using the - full width of the output - • i3bar: fix relative_x and width properties of click events - • i3bar: fix the tray disappearing in some cases when using "tray_output" - • fix various memory leaks and memory correctness issues - • refocus focused window on FOCUS_IN events for the root window. This - fixes incorrect behavior with steam and some tk apps - • fix focus bugs when moving unfocused containers - • fix incorrect urgent window state edge case - • moving an unfocused container from inside a split container to another - workspace doesn’t focus siblings - • toggling and killing floating windows now maintains focus order - • don’t incorrectly focus siblings when scrolling on window decorations - • fix crash when moving a container to a marked workspace - • fix swap when first is behind a fullscreen window - • fix crash when renaming an existing workspace to a name assigned to the - focused output - • reframe swallowed windows if depth doesn’t match - • use detectable autorepeat so that --release bindings are run only when - the key is actually released (and not when it is repeated) - • fix border artifacts when moving windows - • correctly handle bindings for the same mod key with and without --release - • reset B_UPON_KEYRELEASE_IGNORE_MODS bindings when switching modes - • fix height offset calculation in pango text drawing - • fix detection of libiconv on OpenBSD - • free workspace assignments on reload - • fix mouse position at startup with multiple outputs - • no longer allow dragging global fullscreen floating containers - • fix rendering artifacts with global fullscreen containers - • fix disabling floating for scratchpad windows - • fix a crash when renaming an unfocused empty workspace matching an - assignment - • ensure containers have a size of at least 1px after resize - • permit invalid UTF-8 in layout JSON files (e.g. for window titles) - • correct invalid UTF-8 characters in window and container titles - • fix a crash when moving to a child of a floating container - • fix a crash when matching __focused__ with no window open - • fix no_focus when only using floating windows - • fix max_aspect calculation - • moving an unfocused container from another output now maintains - the correct focus order - • don’t change focus order when swapping containers - • correctly update _NET_CURRENT_DESKTOP when moving containers between outputs - using the directional move command - • don’t produce move events after attempting to directionally move a container - towards a direction it can’t go - • fix sticky focus when switching to workspace on different output - - - ┌────────────────────────────┐ - │ Thanks! │ - └────────────────────────────┘ - -Thanks for testing, bugfixes, discussions and everything I forgot go out to: - - Adrian Cybulski, Aestek, Alan Barr, Andriy Yablonskyy, Cassandra Fox, - Christian Duerr, Dan Elkouby, downzer0, Elouan Martinet, Felix Buehler, - Gravemind, Harry Lawrence, Hritik Vijay, hwangcc23, Ingo Bürk, Joona, Klorax, - lasers, Łukasz Adamczak, Martin, Michael Stapelberg, Oliver Graff, - Orestis Floros, Soumya, Takashi Iwai, Thomas Fischer, Todd Walton, Tony - Crisci, Uli Schlachter, Vivien Didelot - --- Michael Stapelberg, 2018-11-04 diff --git a/RELEASE-NOTES-4.17 b/RELEASE-NOTES-4.17 new file mode 100644 index 00000000..d5070705 --- /dev/null +++ b/RELEASE-NOTES-4.17 @@ -0,0 +1,96 @@ + + ┌────────────────────────────┐ + │ Release notes for i3 v4.17 │ + └────────────────────────────┘ + +This is i3 v4.17. This version is considered stable. All users of i3 are +strongly encouraged to upgrade. + +This release contains a number of assorted fixes and improvements across pretty +much all individual components of i3. + + ┌────────────────────────────┐ + │ Changes in i3 v4.17 │ + └────────────────────────────┘ + + • config: make binding modes case-sensitive + • default config: mention ~/.config/i3/config + • default config: start xss-lock, nm-applet, pactl (volume keys) + • docs/userguide: update syntax in strip_workspace_* + • docs/userguide: add a section about hidpi displays + • docs/userguide: document mark --replace + • docs/userguide: uncomment and update mark section example + • docs/userguide: point out differences of normal/pixel title bars + • docs/userguide: clarify which config directives can be used at runtime + • docs/userguide: for_window is a directive, not a command + • docs/ipc: clarify event/reply types + • docs/ipc: mention new i3-ipc++ C++ library + • docs/ipc: clarify restart/exit behavior + • docs/i3bar-protocol: add markup + • man/i3.man: fix config file search order + • ipc: make restart command send a reply once restart completed + • ipc: use queue for all messages + fixes i3bar issues when switching between workspaces with many windows + • i3-dump-log: clarify log message + • i3-msg: exit with status code 2 when i3 returns an error + • render left and right borders of titles in stacked mode + • make swap work with floating windows, fix swap crash + • switch to clang-format-6.0 + • add input and bounding shapes support + (e.g. for the https://github.com/phw/peek screen recorder) + • preserve back_and_forth across restarts + • allow partial UTF-8 to UCS-2 conversion for better handling of + title bar content which cannot be represented (e.g. emoji) + when using bitmap pixel fonts + • check for duplicate key bindings in i3 -C + • i3bar: support transparency via --transparency flag (RGBA) + • i3bar: support for user-defined border widths + + ┌────────────────────────────┐ + │ Bugfixes │ + └────────────────────────────┘ + + • build: correctly depend on glib (for g_utf8_make_valid) + • build: fix build when git is configured to show signatures + • ipc: report correct workspace in init event after workspace move + • ipc: send missing window:focus event + • i3bar: correctly recognize click events with text alignment + • i3bar: fix running without fd 0 + • i3bar: correctly handle button presses on separator + • i3 --moreversion: warn when $DISPLAY is unset + • i3bar: support disabling click events + • release.sh: persist correct version number in docs + • accept output names containing spaces (e.g. in assignment) + • fix cursor resizing positioning + • fix aspect ratio issues (e.g. with mpv) + • fix brief focus flicker when renaming workspaces + • fix crash when canceling i3 via ctrl+c + • fix heap-use-after-free, memory leak + • fix focus bugs in enabling/disabling RandR outputs + • fix crash with popups when fullscreen is non-leaf + • fix crash when moving a second window to mark + • fix crash with programs with splash screen + • fix atoms when closing inactive workspace + • apply title_align to non-leaf containers + • layout loading: correctly mark non-leaf containers + • truncate wm_name utf8 strings to first zero byte + (makes window titles work with buggy clients) + • fix crash in workspace moving + • export I3SOCK environment variable (again) + • fix hanging flaky testcase by using the correct X11 connection + • resize: add missing error replies + • don’t pop up floating windows on the wrong workspace + • remove extra \n from errx and die calls + + ┌────────────────────────────┐ + │ Thanks! │ + └────────────────────────────┘ + +Thanks for testing, bugfixes, discussions and everything I forgot go out to: + + aksel, Albert Safin, Alejandro Angulo, Christopher Hasse, Connor E, Hamish + Macdonald, Ingo Bürk, Iskustvo, Jeffrey Huxen, Jeremy Klotz, Jonathan + Woodlief, lasers, Morten Linderud, nejni-marji, Nguyễn Thái Ngọc Duy, Nils + ANDRÉ-CHANG, Oliver Kraitschy, Orestis Floros, TAL, Vladimir Panteleev + +-- Michael Stapelberg, 2019-08-03 diff --git a/configure.ac b/configure.ac index ec1d9d03..849ae5de 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # Run autoreconf -fi to generate a configure script from this file. AC_PREREQ([2.69]) -AC_INIT([i3], [4.16], [https://github.com/i3/i3/issues]) +AC_INIT([i3], [4.17], [https://github.com/i3/i3/issues]) # For AX_EXTEND_SRCDIR AX_ENABLE_BUILDDIR AM_INIT_AUTOMAKE([foreign subdir-objects -Wall no-dist-gzip dist-bzip2]) From 18f50893f6579e53abf4fa83acba4048c6bca3b2 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sat, 3 Aug 2019 15:14:52 +0200 Subject: [PATCH 98/98] Set non-git version to 4.17-non-git. --- I3_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/I3_VERSION b/I3_VERSION index 82c20487..4aa58fe3 100644 --- a/I3_VERSION +++ b/I3_VERSION @@ -1 +1 @@ -4.17 (2019-08-03) +4.17-non-git