[i34wm][i3ass][i3fyra] --rotate|--move [request] #231

Open
opened 2025-12-27 16:20:17 +00:00 by 1ntronaut · 17 comments
1ntronaut commented 2025-12-27 16:20:17 +00:00 (Migrated from github.com)
  • i3fyra --rotate [sibling|family] is a great succes and very welcome addition to [i34wm], works like a charm.

Excuse me if this is actually two request, in stead of one; but the behaviour is so similar I thought I'd of them as one in the first place (see idea 1 and idea 2 respectively)

Would it be possible to make i3fyra rotate in order of containers in i3ass? This will probably/maybe require yet another big chunk of rewriting code (or not, i don't know?).

  • please add short option i3fyra -R for i3fyra --rotate`

Idea 1

  • This would basically be a conditionally invoked, but sequential i3fyra --rotate/i3fyra --move commands, issued to the PARENT I3FYRA CONTAINER (and only the VISUAL ones, depending

  • i3fyra --rotate [right|clockwise|clock|c] would cause the following movement of containers:
    (( the opposite behaviour will probably be named i3fyra --rotate [left|anticlockwise|aclock|a] ))

example

on my screen (container C focused):
A B
C D

i3fyra --rotate [right|clockwise|clock|c] would give me
C B
A D

another `i3fyra --rotate [right|clockwise|clock|c]' would give me
A C
D B

another `i3fyra --rotate [right|clockwise|clock|c]' would give me
D A
B C

another i3fyra --rotate [right|clockwise|clock|c] would return to the original
A B
C D


I've considered the following breaking changes in regards to my request above:

1) Of course i3fyra --rotate [right|clockwise|clock|c] should only 'rotate' through VISIBLE containers, meaning if:

on my screen:
A B
A D

i3fyra --rotate [right|clockwise|clock|c] would give me
D A
D B

i3fyra --rotate [right|clockwise|clock|c] would give me
B D
B A

another `i3fyra --rotate [right|clockwise|clock|c]' would return to the original
A B
A D

2) just a minor afterthought; if:

on my screem
C B
C B

i3fyra --rotate [right|clockwise|clock|c] would give me
B C
B C

at this point, i3fyra --rotate [right|clockwise|clock|c] would essentially be the same as i3fyra --rotate family`


Idea 2

I'd like a similar function not moving** the i3fyra CONTAINERS around, but only the focused container..

In my mind it could for example be an additional argument to i3fyra --move and those would be i3fyra --move [clockwise|clock|c] and i3fyra --move anticlockwise|aclock|a].
This would still fit the current scripts, as the --move option for a destination i3fyra CONTAINER is CaPs-SeNsItIvE as of now, anyway.
Another contender would be i3fyra --cycle [right|r|clockwise|clock|c vs i3fyra --cycle [left|l|anticlockwise|aclock|a]

example:
on my screen, window in i3fyra CONTAINER C is focused
A B
C* D

i3fyra --move [clockwise|clock|c] would function the same as i3fyra --move up
A* B
C D

another i3fyra --move [clockwise|clock|c] would function the same as i3fyra --move right
A B*
C D

another i3fyra --move [clockwise|clock|c] would function the same as i3fyra --move down
A B
C D*

another i3fyra --move [clockwise|clock|c] would return to the original state
A B
C* D

1) I've considered more breaking features for this one, but it basically boils down to being able to consider which i3fyra CONTAINERS are currently VISIBLE (otherwise, the command i3fyra --move will triger (un)hiding the next container, in stead of cycling through the VISIBLE containers.


THanks for the amazing work @budRich ; love the ass.

Would above functinoality be possible within i3ass, or is this a case of 'you should write your personal bash script'

- `i3fyra --rotate [sibling|family]` is a great succes and very welcome addition to [i34wm], works like a charm. Excuse me if this is actually two request, in stead of one; but the behaviour is so similar I thought I'd of them as one in the first place (see idea 1 and idea 2 respectively) Would it be possible to make i3fyra rotate in order of containers in i3ass? This will probably/maybe require yet another big chunk of rewriting code (or not, i don't know?). - please add short option `i3fyra -R` for i3fyra --rotate` #### Idea 1 - This would basically be a conditionally invoked, but sequential `i3fyra --rotate`/`i3fyra --move` commands, issued to the PARENT I3FYRA CONTAINER (and only the VISUAL ones, depending - `i3fyra --rotate [right|clockwise|clock|c]` would cause the following movement of containers: (( the opposite behaviour will probably be named `i3fyra --rotate [left|anticlockwise|aclock|a]` )) example --- on my screen (container C focused): A B C D `i3fyra --rotate [right|clockwise|clock|c]` would give me C B A D another `i3fyra --rotate [right|clockwise|clock|c]' would give me A C D B another `i3fyra --rotate [right|clockwise|clock|c]' would give me D A B C another `i3fyra --rotate [right|clockwise|clock|c]` would return to the original A B C D --- I've considered the following breaking changes in regards to my request above: **1)** Of course `i3fyra --rotate [right|clockwise|clock|c]` should only 'rotate' through VISIBLE containers, meaning if: on my screen: A B A D `i3fyra --rotate [right|clockwise|clock|c]` would give me D A D B `i3fyra --rotate [right|clockwise|clock|c]` would give me B D B A another `i3fyra --rotate [right|clockwise|clock|c]' would return to the original A B A D **2)** just a minor afterthought; if: on my screem C B C B `i3fyra --rotate [right|clockwise|clock|c]` would give me B C B C at this point, `i3fyra --rotate [right|clockwise|clock|c] would essentially be the same as `i3fyra --rotate family` --- #### Idea 2 I'd like a similar function **not** moving** the i3fyra CONTAINERS around, but only the focused container.. In my mind it could for example be an additional argument to `i3fyra --move` and those would be `i3fyra --move [clockwise|clock|c]` and `i3fyra --move anticlockwise|aclock|a]`. This would still fit the current scripts, as the `--move` option for a destination i3fyra CONTAINER is CaPs-SeNsItIvE as of now, anyway. Another contender would be `i3fyra --cycle [right|r|clockwise|clock|c` vs `i3fyra --cycle [left|l|anticlockwise|aclock|a]` example: on my screen, window in i3fyra CONTAINER C is focused A B C* D `i3fyra --move [clockwise|clock|c]` would function the same as `i3fyra --move up` A* B C D another `i3fyra --move [clockwise|clock|c]` would function the same as `i3fyra --move right` A B* C D another `i3fyra --move [clockwise|clock|c]` would function the same as `i3fyra --move down` A B C D* another `i3fyra --move [clockwise|clock|c]` would return to the original state A B C* D **1)** I've considered more breaking features for this one, but it basically boils down to being able to consider which i3fyra CONTAINERS are currently VISIBLE (otherwise, the command `i3fyra --move` will triger (un)hiding the next container, in stead of cycling through the VISIBLE containers. --- THanks for the amazing work @budRich ; love the ass. Would above functinoality be possible within i3ass, or is this a case of 'you should write your personal bash script'
budRich commented 2025-12-27 17:09:52 +00:00 (Migrated from github.com)

Well everything is possible, and i can very much see how both ideas can be useful. The complicated part is to have clockwise/anti-clockwise move in different directions, to do that we need to keep track of the previous actual direction (u,r,l,d) that clockwise was. due to the nature of i3ass, where depending on what we have done to the layout since last clockwise action, it will be very difficult have clockwise mean what we want.

So i wonder if the feature(s) instead can't be shaped into u,r,l,d at least the idea1, where as i understand we basically swap location of the containers while keeping the size of the locations constant, i think this could be hooked in to the current move system without breaking or writing too much code.

But for idea2, as of now, i think it will be too hard to do it in a generic way where it reliably works on a layout in motion. but if you know your layout and know that left is left and up is up and B is B it is not difficult to achieve it. There is proably an elegant solution to make it reliable and generic, but i can't see it now.

I have had this feeling for as long as this project existed, that i can replace 1000 lines of my calculating positions stuff with a 5 line formula that just works. The relation between four directions and four quadrants is what it's all about, but, i have not found the solution, i3ass is just a polished brute force to this problem.


in summary, i will meditate on idea1 but probably with u,r,l,d logic. I think this is a completely different thing than the current --rotate, and i now want to rename current --rotate to --swap.

I get worried with the whole clockwise catch all directions, it gets weird with time, i usually have my desktop sessions running for weeks, and it is not unlikely that in such a session it goes days between firing the command. at the same time, i can definitely see it as sweet to just bash one key combo, to cycle a window till it fits.


Hmm, maybe it doesn't matter that we keep track of the direction, clockwise is not dependent on the previous motion, but the current location of the container.. yeah, forget all i wrote before i will try to do a generic focus clockwise script. if i get that working it will be easy i think to morph it into idea1 and idea2

Well everything is possible, and i can very much see how both ideas can be useful. The complicated part is to have clockwise/anti-clockwise move in different directions, to do that we need to keep track of the previous actual direction (u,r,l,d) that clockwise was. due to the nature of i3ass, where depending on what we have done to the layout since last clockwise action, it will be very difficult have clockwise mean what we want. So i wonder if the feature(s) instead can't be shaped into u,r,l,d at least the idea1, where as i understand we basically swap location of the containers while keeping the size of the locations constant, i think this could be hooked in to the current move system without breaking or writing too much code. But for idea2, as of now, i think it will be too hard to do it in a generic way where it reliably works on a layout in motion. but if you know your layout and know that left is left and up is up and B is B it is not difficult to achieve it. There is proably an elegant solution to make it reliable and generic, but i can't see it now. I have had this feeling for as long as this project existed, that i can replace 1000 lines of my calculating positions stuff with a 5 line formula that just works. The relation between four directions and four quadrants is what it's all about, but, i have not found the solution, i3ass is just a polished brute force to this problem. --- in summary, i will meditate on idea1 but probably with u,r,l,d logic. I think this is a completely different thing than the current --rotate, and i now want to rename current --rotate to --swap. I get worried with the whole clockwise catch all directions, it gets weird with time, i usually have my desktop sessions running for weeks, and it is not unlikely that in such a session it goes days between firing the command. at the same time, i can definitely see it as sweet to just bash one key combo, to cycle a window till it fits. --- Hmm, maybe it doesn't matter that we keep track of the direction, clockwise is not dependent on the previous motion, but the current location of the container.. yeah, forget all i wrote before i will try to do a generic focus clockwise script. if i get that working it will be easy i think to morph it into idea1 and idea2
1ntronaut commented 2025-12-27 20:10:23 +00:00 (Migrated from github.com)

hi @budRich

...

I was kinda confused for a sec w/ what you wrote before, but yeah, so....

in summary, i will meditate on idea1 but probably with u,r,l,d logic. I think this is a completely different thing than the current --rotate, and i now want to rename current --rotate to --swap.

You are right, it IS a completely different thing, maybe the name of rotate made me think about it in the first place, .
+
In the meantime, I had actually already made an alias i34swap='i3fyra --rotate' ;p. We hive-minded there.
It's the same feeling probably as to why i3fyra --orientation toggle could've been i3fyra --mirror (don't get itchy fingers right now and change the syntax on me!)

I get worried with the whole clockwise catch all directions, it gets weird with time, i usually have my desktop sessions running for weeks, and it is not unlikely that in such a session it goes days between firing the command. at the same time, i can definitely see it as sweet to just bash one key combo, to cycle a window till it fits.

I think the quote above is the exact thing that makes the idea click (as in, you understand what I meant).

( I might even go as far as to say such a feature will make dwm default functionality (as well as xmonad's) as a dynamic tiling manager feasible, but it will maintain i3wm manual tiling capablities as to edit your window managament as you wish 'on the fly'. )

Hmm, maybe it doesn't matter that we keep track of the direction, clockwise is not dependent on the previous motion, but the current location of the container.. yeah, forget all i wrote before i will try to do a generic focus clockwise script. if i get that working it will be easy i think to morph it into idea1 and idea2

Yeah, I'd figure it might be possible exactly because you don't have to keep track of the previous movement. That is, because i3ass already kind of 'keeps track' because it has family/sibling/cousin relationships right there in the i3list array, as well as knowing when to hide or not hide, depending on visible/invisible containers...

hi @budRich > ... I was kinda confused for a sec w/ what you wrote before, but yeah, so.... > in summary, i will meditate on idea1 but probably with u,r,l,d logic. I think this is a completely different thing than the current --rotate, and i now want to rename current --rotate to --swap. You are right, it **IS** a completely different thing, maybe the name of rotate made me think about it in the first place, . + In the meantime, I had actually already made an `alias i34swap='i3fyra --rotate'` ;p. We hive-minded there. It's the same feeling probably as to why `i3fyra --orientation toggle` could've been `i3fyra --mirror` (don't get itchy fingers right now and change the syntax on me!) > I get worried with the whole clockwise catch all directions, it gets weird with time, i usually have my desktop sessions running for weeks, and it is not unlikely that in such a session it goes days between firing the command. at the same time, i can definitely see it as sweet to just bash one key combo, to cycle a window till it fits. I think the quote above is the exact thing that makes the idea click (as in, you understand what I meant). ( I might even go as far as to say such a feature will make dwm default functionality (as well as xmonad's) as a _dynamic_ tiling manager feasible, but it will maintain i3wm _manual_ tiling capablities as to edit your window managament as you wish 'on the fly'. ) > Hmm, maybe it doesn't matter that we keep track of the direction, clockwise is not dependent on the previous motion, but the current location of the container.. yeah, forget all i wrote before i will try to do a generic focus clockwise script. if i get that working it will be easy i think to morph it into idea1 and idea2 Yeah, I'd figure it _might_ be possible exactly **because** you don't have to keep track of the previous movement. That is, because i3ass already kind of 'keeps track' because it has family/sibling/cousin relationships right there in the i3list array, as well as knowing when to hide or not hide, depending on visible/invisible containers...
budRich commented 2025-12-27 21:42:34 +00:00 (Migrated from github.com)

Im gonna think out loud a bit here, i think the "easiest" will be to think of the operations as swaps,

this is output of i3list with A container focused:

i3list[TFT]=B                          # Target Window twin
i3list[TFC]=D                          # Target Window cousin
i3list[TFS]=C                          # Target Window sibling

I think the secret formula is figuring out where we want to go, considering regular ABCD grid, A clockwise want to go to B, if B doesn't exist, it is D and last C. When we know where to go we swap those containers, say all four containers are visible, we swap A with B:

BA
CD

now, we swap B with D ie swap swapped with it's first prio.

DA
CB

and finally C with D ie swap swapped with previously untouced.

CA
DB

If target (A) has no sibling (C):

AB
AD

it is the same formula:

Switch A with B:

BA
BD

Swap the swapped with it's old sibling, ie swap swapped with previously untouced:

DA
DB

If target has no cousin (D):

AB
CB

Same first step, but next swap is with old cousins, e swap swapped with previously untouced:

BA     CA
CA  -> BA

Targets first prio (twin, B) not visible:

AD
CD

Swap A with Prio2 (D), swap swapped with previously untouced:

DA    CA
CA -> DA

otherwise we have less than 3 siblings, ie swap or do nothing (only 1 container visible).


This is where i need to brute force, because prio looks like this:

A: B(twin),D(cousin),C(sibling)
B: D(sibling),C(cousin),A(twin)
C: A(sibling),B(cousin),D(twin)
D: C(twin),A(cousin),B(sibling)

hmm, there is a pattern, A and D and B and C has the same prio, all has cousin as second prio.

Knowing where to go with three containers it is the same formula, with four containers we also need to know the prio for the prio of the first swapped container.

Yeah, this will work if some CS math genius stumble upon this and can spot some bitflip magic we can use here instead, please necrobump this issue.

Im gonna think out loud a bit here, i think the "easiest" will be to think of the operations as swaps, this is output of i3list with A container focused: ``` i3list[TFT]=B # Target Window twin i3list[TFC]=D # Target Window cousin i3list[TFS]=C # Target Window sibling ``` I think the secret formula is figuring out where we want to go, considering regular ABCD grid, A clockwise want to go to B, if B doesn't exist, it is D and last C. When we know where to go we swap those containers, say all four containers are visible, we swap A with B: ``` BA CD ``` now, we swap B with D ie swap swapped with it's first prio. ``` DA CB ``` and finally C with D ie swap swapped with previously untouced. ``` CA DB ``` ---- If target (A) has no sibling (C): ``` AB AD ``` it is the same formula: Switch A with B: ``` BA BD ``` Swap the swapped with it's old sibling, ie swap swapped with previously untouced: ``` DA DB ``` --- If target has no cousin (D): ``` AB CB ``` Same first step, but next swap is with old cousins, e swap swapped with previously untouced: ``` BA CA CA -> BA ``` --- Targets first prio (twin, B) not visible: ``` AD CD ``` Swap A with Prio2 (D), swap swapped with previously untouced: ``` DA CA CA -> DA ``` otherwise we have less than 3 siblings, ie swap or do nothing (only 1 container visible). --- This is where i need to brute force, because prio looks like this: ``` A: B(twin),D(cousin),C(sibling) B: D(sibling),C(cousin),A(twin) C: A(sibling),B(cousin),D(twin) D: C(twin),A(cousin),B(sibling) ``` hmm, there is a pattern, A and D and B and C has the same prio, all has cousin as second prio. Knowing where to go with three containers it is the same formula, with four containers we also need to know the prio for the prio of the first swapped container. Yeah, this will work if some CS math genius stumble upon this and can spot some bitflip magic we can use here instead, please necrobump this issue.
budRich commented 2025-12-27 21:47:53 +00:00 (Migrated from github.com)

also counterclockwise prio:

A: C(sibling),D(cousin),B(twin)
B: A(twin),C(cousin),D(sibling)
C: D(twin),B(cousin),A(sibling)
D: B(sibling),A(cousin),C(twin)

compared to clockwise
A: B(twin),D(cousin),C(sibling)
B: D(sibling),C(cousin),A(twin)
C: A(sibling),B(cousin),D(twin)
D: C(twin),A(cousin),B(sibling)

cousin is always second prio, so first and last is one or the other..

also counterclockwise prio: ``` A: C(sibling),D(cousin),B(twin) B: A(twin),C(cousin),D(sibling) C: D(twin),B(cousin),A(sibling) D: B(sibling),A(cousin),C(twin) compared to clockwise A: B(twin),D(cousin),C(sibling) B: D(sibling),C(cousin),A(twin) C: A(sibling),B(cousin),D(twin) D: C(twin),A(cousin),B(sibling) ``` cousin is always second prio, so first and last is one or the other..
budRich commented 2025-12-28 10:14:48 +00:00 (Migrated from github.com)

I scripted:

#!/bin/bash

# usage:   cycla [CW|CC] [focus|swap]
# default: cycla CW focus

direction=${1:-CW}
action=${2:-focus}

declare -A i3list prio layout
eval "$(i3list)"

current_container=${i3list[AWP]}

if [[ ${direction^^} = CW ]]; then
  layout=(
    [A]=C [B]=A
    [C]=D [D]=B
  )
else # CC
  layout=(
    [A]=B [B]=D
    [C]=A [D]=C
  )
fi

untouched=${i3list[LVI]}

swapp() {

  local src=$1 trg=$2 msg srcID trgID

  [[ $action = swap ]] || return

  srcID=${i3list[C${src}P]}
  trgID=${i3list[C${trg}P]}
  i3list[C${src}P]=$trgID
  i3list[C${trg}P]=$srcID

  # this will remove the old mark
  msg+="[con_id=$srcID] mark --toggle --add i34${src};"
  msg+="[con_id=$trgID] mark --toggle --add i34${trg};"

  # add new mark
  msg+="[con_id=$srcID] mark --toggle --add i34${trg};"
  msg+="[con_id=$trgID] mark --toggle --add i34${src};"

  msg+="[con_id=$srcID] swap with con_id $trgID;"

  i3-msg "$msg"
}

number_of_visibles=${#i3list[LVI]}

case "$number_of_visibles" in
  4 ) 
    sw1=${layout[$current_container]}
    sw2=${layout[$sw1]}
    untouched=${untouched//[${sw1}${current_container}${sw2}]/}
    swapp "$current_container" "$sw1"
    swapp "$sw1" "$sw2"
    swapp "$sw2" "$untouched"
  ;;
  3 )
    missing=${i3list[LAL]//[${i3list[LVI]}]/}
    unset 'layout[$missing]'
    sw1=${layout[$current_container]}

    # cousin is always second choice
    [[ ${i3list[LVI]} =~ $sw1 ]] || sw1=${i3list[AFC]}
    untouched=${untouched//[${sw1}${current_container}]/}
    
    swapp "$current_container" "$sw1"
    swapp "$sw1" "$untouched"

    layout[$current_container]=$sw1
    layout[$sw1]=$untouched
    layout[$untouched]=$current_container
  ;;
  2 )
    unset 'layout[@]'
    remaining=${i3list[LVI]//$current_container/}
    layout[$current_container]=$remaining

    swapp "$current_container" "$remaining"
  ;;
  1 ) echo we are one no swapping ;;
  * ) echo "broken layout"        ;;
esac

if [[ $action = focus ]]; 
  then trg_con=${i3list[C${layout[$current_container]}F]}
  else trg_con=${i3list[AWC]}
fi

i3-msg "[con_id=${trg_con}] focus"

You need the latest next version for it to work. One thing i realized is that we need to decide what the swap actually does here. As the script works now it is a full swap, i.e cycle swap full layout, will shuffle all containers and rename the marks, which means that i3king will now plop new windows into the renamed container, which is different from how other i3fyra container shuffling works, where i have virtual positions that keep track of where the OG containers are. I think you understand what i mean, and if not, try shuffling containers with the script and trigger i3king positioning.

I could hook this up with the virtual position thing as well, but i am not sure if that is what we want? I think of it like this: My personal idea is that the four containers represent a shape ( long(A), square(B), tall(D), main/large(C) ) and different applications fit different shapes, so when i3king positions a window it prioritize the shape. But applications can also be grouped by function, say two main applications like: text editor and browser, i want them in the same group.

So the problem now is that if i cycle the main container with browser in it, to the shape of square and spawn a text editor, it will be positioned in the main/large container but not grouped with the browser.

To be honest, it feels kind of weird, and i can just imagine it being a lot more weird for someone not part of this discussion.


Above problem is only relevant to swap, implementing move (idea2) is trivial, and that will not cause any other issues so i can do that, but i am wondering if maybe "cycle swap" is a too complex feature to include in i3ass.


Also there are still lots of corner cases not covered by the script, it obviously only works on i3fyra ws, but i never added any tests for that, and it will not work perfectly if the layout of the containers aren't tabbed. but these are easy to fix.

I scripted: ```bash #!/bin/bash # usage: cycla [CW|CC] [focus|swap] # default: cycla CW focus direction=${1:-CW} action=${2:-focus} declare -A i3list prio layout eval "$(i3list)" current_container=${i3list[AWP]} if [[ ${direction^^} = CW ]]; then layout=( [A]=C [B]=A [C]=D [D]=B ) else # CC layout=( [A]=B [B]=D [C]=A [D]=C ) fi untouched=${i3list[LVI]} swapp() { local src=$1 trg=$2 msg srcID trgID [[ $action = swap ]] || return srcID=${i3list[C${src}P]} trgID=${i3list[C${trg}P]} i3list[C${src}P]=$trgID i3list[C${trg}P]=$srcID # this will remove the old mark msg+="[con_id=$srcID] mark --toggle --add i34${src};" msg+="[con_id=$trgID] mark --toggle --add i34${trg};" # add new mark msg+="[con_id=$srcID] mark --toggle --add i34${trg};" msg+="[con_id=$trgID] mark --toggle --add i34${src};" msg+="[con_id=$srcID] swap with con_id $trgID;" i3-msg "$msg" } number_of_visibles=${#i3list[LVI]} case "$number_of_visibles" in 4 ) sw1=${layout[$current_container]} sw2=${layout[$sw1]} untouched=${untouched//[${sw1}${current_container}${sw2}]/} swapp "$current_container" "$sw1" swapp "$sw1" "$sw2" swapp "$sw2" "$untouched" ;; 3 ) missing=${i3list[LAL]//[${i3list[LVI]}]/} unset 'layout[$missing]' sw1=${layout[$current_container]} # cousin is always second choice [[ ${i3list[LVI]} =~ $sw1 ]] || sw1=${i3list[AFC]} untouched=${untouched//[${sw1}${current_container}]/} swapp "$current_container" "$sw1" swapp "$sw1" "$untouched" layout[$current_container]=$sw1 layout[$sw1]=$untouched layout[$untouched]=$current_container ;; 2 ) unset 'layout[@]' remaining=${i3list[LVI]//$current_container/} layout[$current_container]=$remaining swapp "$current_container" "$remaining" ;; 1 ) echo we are one no swapping ;; * ) echo "broken layout" ;; esac if [[ $action = focus ]]; then trg_con=${i3list[C${layout[$current_container]}F]} else trg_con=${i3list[AWC]} fi i3-msg "[con_id=${trg_con}] focus" ``` You need the latest next version for it to work. One thing i realized is that we need to decide what the swap actually does here. As the script works now it is a full swap, i.e cycle swap full layout, will shuffle all containers and rename the marks, which means that i3king will now plop new windows into the renamed container, which is different from how other i3fyra container shuffling works, where i have virtual positions that keep track of where the OG containers are. I think you understand what i mean, and if not, try shuffling containers with the script and trigger i3king positioning. I could hook this up with the virtual position thing as well, but i am not sure if that is what we want? I think of it like this: My personal idea is that the four containers represent a shape ( long(A), square(B), tall(D), main/large(C) ) and different applications fit different shapes, so when i3king positions a window it prioritize the shape. But applications can also be grouped by function, say two main applications like: text editor and browser, i want them in the same group. So the problem now is that if i cycle the main container with browser in it, to the shape of square and spawn a text editor, it will be positioned in the main/large container but not grouped with the browser. To be honest, it feels kind of weird, and i can just imagine it being a lot more weird for someone not part of this discussion. --- Above problem is only relevant to swap, implementing move (idea2) is trivial, and that will not cause any other issues so i can do that, but i am wondering if maybe "cycle swap" is a too complex feature to include in i3ass. --- Also there are still lots of corner cases not covered by the script, it obviously only works on i3fyra ws, but i never added any tests for that, and it will not work perfectly if the layout of the containers aren't tabbed. but these are easy to fix.
budRich commented 2025-12-28 11:19:33 +00:00 (Migrated from github.com)

i went ahead and added move function also, just wanted to feel how it felt:

#!/bin/bash

# usage:   cycla [CW|CC] [focus|swap|move]
# default: cycla CW focus

declare -g  direction msg action current_container untouched
declare -Ag i3list layout

eval "$(i3list)"

direction=${1:-CW}
action=${2:-focus}
untouched=${i3list[LVI]}

current_container=${i3list[AWP]}

if [[ ${direction^^} = CW ]]; then
  layout=(
    [A]=C [B]=A
    [C]=D [D]=B
  )
else # CC
  layout=(
    [A]=B [B]=D
    [C]=A [D]=C
  )
fi

swapp() {

  local src=$1 trg=$2 srcID trgID

  [[ $action = swap ]] || return

  srcID=${i3list[C${src}P]}
  trgID=${i3list[C${trg}P]}
  i3list[C${src}P]=$trgID
  i3list[C${trg}P]=$srcID

  # this will remove the old mark
  msg+="[con_id=$srcID] mark --toggle --add i34${src},"
  msg+="[con_id=$trgID] mark --toggle --add i34${trg},"

  # add new mark
  msg+="[con_id=$srcID] mark --toggle --add i34${trg},"
  msg+="[con_id=$trgID] mark --toggle --add i34${src},"

  msg+="[con_id=$srcID] swap with con_id $trgID,"
}

number_of_visibles=${#i3list[LVI]}

case "$number_of_visibles" in
  4 ) 
    sw1=${layout[$current_container]}
    sw2=${layout[$sw1]}
    untouched=${untouched//[${sw1}${current_container}${sw2}]/}
    swapp "$current_container" "$sw1"
    swapp "$sw1" "$sw2"
    swapp "$sw2" "$untouched"
  ;;
  3 )
    missing=${i3list[LAL]//[${i3list[LVI]}]/}
    unset 'layout[$missing]'
    sw1=${layout[$current_container]}

    # cousin is always second choice
    [[ ${i3list[LVI]} =~ $sw1 ]] || sw1=${i3list[AFC]}
    untouched=${untouched//[${sw1}${current_container}]/}
    
    swapp "$current_container" "$sw1"
    swapp "$sw1" "$untouched"

    layout[$current_container]=$sw1
    layout[$sw1]=$untouched
    layout[$untouched]=$current_container
  ;;
  2 )
    unset 'layout[@]'
    remaining=${i3list[LVI]//$current_container/}
    layout[$current_container]=$remaining

    swapp "$current_container" "$remaining"
  ;;
  1 ) echo we are one no swapping ;;
  * ) echo "broken layout"        ;;
esac

case "$action" in
  focus ) 
    msg+="[con_id=${i3list[C${layout[$current_container]}F]}] focus" ;;
  swap  ) 
    msg+="[con_id=${i3list[AWC]}] focus" ;;
  move  )
    msg+="[con_id=${i3list[AWC]}] focus, move to mark i34${layout[$current_container]}, focus" ;;
esac

[[ $msg ]] && i3-msg "$msg"

and it is good, move and focus cycle are both super nice. But i don't think swap in this form is really useful. i wonder if it is possible to hook it into i3fyra, and single key cycle will shuffle the layout, but retain the shape and groups, this would mean given we have the shapes i described earlier ( long(A), square(B), main/large(C), tall(D) ) , if main(C) is focused and we cycle swap CC, we end up with main at D . If size and group of main is retained, it will mean that no matter how we look at it, long will now be in B. And that breaks the cycle, since cycling like we do in the script would have meant that long(A) would become long(C), and tall(D) would be tall(B) above large/main(D).

I think in my shape oriented layout i could make a script that single key cycles and follows my rules , rule is that families stay intact, long is above/below main and square is above/below tall. with that in mind cycling C CC -> D will also imply B CC -> A, so A -> B and D -> C, which is just a normal mirror swap (which can be done with the new --swap family), continue cycling C CC will mirror it with B and i don't think we really need to swap the remaining two containers here? cycling C (now at B) CC -> A, we now need to put long in C, not touching the remaining two containers, this could also be done with the new --swap family.. hmm, this might actually be what we should do, it will let us cycle the position of a single container around the grid with a single command. Maybe this is exactly what you said before, never the less i will try to implement it.

The rule can also be generic and is not about shapes it goes like this: when we trigger cycle we will get a target container (this is layout[] array in the script), if target container is not in the same family as current container, we --swap family, otherwise we --swap sibling !

i went ahead and added move function also, just wanted to feel how it felt: ```bash #!/bin/bash # usage: cycla [CW|CC] [focus|swap|move] # default: cycla CW focus declare -g direction msg action current_container untouched declare -Ag i3list layout eval "$(i3list)" direction=${1:-CW} action=${2:-focus} untouched=${i3list[LVI]} current_container=${i3list[AWP]} if [[ ${direction^^} = CW ]]; then layout=( [A]=C [B]=A [C]=D [D]=B ) else # CC layout=( [A]=B [B]=D [C]=A [D]=C ) fi swapp() { local src=$1 trg=$2 srcID trgID [[ $action = swap ]] || return srcID=${i3list[C${src}P]} trgID=${i3list[C${trg}P]} i3list[C${src}P]=$trgID i3list[C${trg}P]=$srcID # this will remove the old mark msg+="[con_id=$srcID] mark --toggle --add i34${src}," msg+="[con_id=$trgID] mark --toggle --add i34${trg}," # add new mark msg+="[con_id=$srcID] mark --toggle --add i34${trg}," msg+="[con_id=$trgID] mark --toggle --add i34${src}," msg+="[con_id=$srcID] swap with con_id $trgID," } number_of_visibles=${#i3list[LVI]} case "$number_of_visibles" in 4 ) sw1=${layout[$current_container]} sw2=${layout[$sw1]} untouched=${untouched//[${sw1}${current_container}${sw2}]/} swapp "$current_container" "$sw1" swapp "$sw1" "$sw2" swapp "$sw2" "$untouched" ;; 3 ) missing=${i3list[LAL]//[${i3list[LVI]}]/} unset 'layout[$missing]' sw1=${layout[$current_container]} # cousin is always second choice [[ ${i3list[LVI]} =~ $sw1 ]] || sw1=${i3list[AFC]} untouched=${untouched//[${sw1}${current_container}]/} swapp "$current_container" "$sw1" swapp "$sw1" "$untouched" layout[$current_container]=$sw1 layout[$sw1]=$untouched layout[$untouched]=$current_container ;; 2 ) unset 'layout[@]' remaining=${i3list[LVI]//$current_container/} layout[$current_container]=$remaining swapp "$current_container" "$remaining" ;; 1 ) echo we are one no swapping ;; * ) echo "broken layout" ;; esac case "$action" in focus ) msg+="[con_id=${i3list[C${layout[$current_container]}F]}] focus" ;; swap ) msg+="[con_id=${i3list[AWC]}] focus" ;; move ) msg+="[con_id=${i3list[AWC]}] focus, move to mark i34${layout[$current_container]}, focus" ;; esac [[ $msg ]] && i3-msg "$msg" ``` and it is good, move and focus cycle are both super nice. But i don't think swap in this form is really useful. i wonder if it is possible to hook it into i3fyra, and single key cycle will shuffle the layout, but retain the shape and groups, this would mean given we have the shapes i described earlier ( long(A), square(B), main/large(C), tall(D) ) , if main(C) is focused and we cycle swap CC, we end up with main at D . If size and group of main is retained, it will mean that no matter how we look at it, long will now be in B. And that breaks the cycle, since cycling like we do in the script would have meant that long(A) would become long(C), and tall(D) would be tall(B) above large/main(D). I think in my shape oriented layout i could make a script that single key cycles and follows my rules , rule is that families stay intact, long is above/below main and square is above/below tall. with that in mind cycling C CC -> D will also imply B CC -> A, so A -> B and D -> C, which is just a normal mirror swap (which can be done with the new --swap family), continue cycling C CC will mirror it with B and i don't think we really need to swap the remaining two containers here? cycling C (now at B) CC -> A, we now need to put long in C, not touching the remaining two containers, this could also be done with the new --swap family.. hmm, this might actually be what we should do, it will let us cycle the position of a single container around the grid with a single command. Maybe this is exactly what you said before, never the less i will try to implement it. The rule can also be generic and is not about shapes it goes like this: when we trigger cycle we will get a target container (this is layout[] array in the script), if target container is not in the same family as current container, we --swap family, otherwise we --swap sibling !
budRich commented 2025-12-28 11:52:39 +00:00 (Migrated from github.com)

i did it, and also realized i forgot to tell i3fyra that i renamed --rotat -> --swap so it didn't work, i fixed that. New script using i3fyra --swap to swap, is a lot more simple (also doesn't depend on the newly added i3list container ID output thing, but whatever)

#!/bin/bash

# usage:   cycla [CW|CC] [focus|swap|move]
# default: cycla CW focus

declare -g  direction action current_container target
declare -Ag i3list layout

eval "$(i3list)"

direction=${1:-CW}
action=${2:-focus}

current_container=${i3list[AWP]}

if [[ ${direction^^} = CW ]]; then
  layout=(
    [A]=C [B]=A
    [C]=D [D]=B
  )
else # CC
  layout=(
    [A]=B [B]=D
    [C]=A [D]=C
  )
fi

number_of_visibles=${#i3list[LVI]}

case "$number_of_visibles" in
  4 ) target=${layout[$current_container]} ;;
  2 ) target=${i3list[LVI]//$current_container/} ;;
  3 )
    target=${layout[$current_container]}

    # cousin (AFC) is always second choice
    [[ ${i3list[LVI]} =~ $target ]] \
      || target=${i3list[AFC]}
  ;;
  1 ) echo we are one, no swapping ;;
  * ) echo "broken layout"         ;;
esac

[[ $target ]] || exit

case "$action" in
  focus ) 
    i3-msg "[con_id=${i3list[C${target}F]}] focus" ;;
  move  )
    i3-msg "[con_id=${i3list[AWC]}] focus, move to mark i34${target}, focus" ;;
  swap  )
    if [[ ${i3list[AFF]} =~ $target ]]
      then i3fyra --swap sibling
      else i3fyra --swap family
    fi
  ;;
esac

I think this version of swap makes more sense, and it requires 0 additional messing with marks or rethinking the purpose of the universe, one side effect that might be confusing is that if you cycle swap a container wihtout a sibling it will always mirror families (dang, now i also want to rename --swap to --mirror, but i think i got burned the last time i tried to rename it)

i did it, and also realized i forgot to tell i3fyra that i renamed --rotat -> --swap so it didn't work, i fixed that. New script using i3fyra --swap to swap, is a lot more simple (also doesn't depend on the newly added i3list container ID output thing, but whatever) ```bash #!/bin/bash # usage: cycla [CW|CC] [focus|swap|move] # default: cycla CW focus declare -g direction action current_container target declare -Ag i3list layout eval "$(i3list)" direction=${1:-CW} action=${2:-focus} current_container=${i3list[AWP]} if [[ ${direction^^} = CW ]]; then layout=( [A]=C [B]=A [C]=D [D]=B ) else # CC layout=( [A]=B [B]=D [C]=A [D]=C ) fi number_of_visibles=${#i3list[LVI]} case "$number_of_visibles" in 4 ) target=${layout[$current_container]} ;; 2 ) target=${i3list[LVI]//$current_container/} ;; 3 ) target=${layout[$current_container]} # cousin (AFC) is always second choice [[ ${i3list[LVI]} =~ $target ]] \ || target=${i3list[AFC]} ;; 1 ) echo we are one, no swapping ;; * ) echo "broken layout" ;; esac [[ $target ]] || exit case "$action" in focus ) i3-msg "[con_id=${i3list[C${target}F]}] focus" ;; move ) i3-msg "[con_id=${i3list[AWC]}] focus, move to mark i34${target}, focus" ;; swap ) if [[ ${i3list[AFF]} =~ $target ]] then i3fyra --swap sibling else i3fyra --swap family fi ;; esac ``` I think this version of swap makes more sense, and it requires 0 additional messing with marks or rethinking the purpose of the universe, one side effect that might be confusing is that if you cycle swap a container wihtout a sibling it will always mirror families (dang, now i also want to rename --swap to --mirror, but i think i got burned the last time i tried to rename it)
budRich commented 2025-12-28 13:08:06 +00:00 (Migrated from github.com)

dang it, i was hoping that using i3viswiz and i3fyra to focus and move would automatically work even for containers that have splith/v layouts, and it kind of do work but because i have been so smarty pants about how to find the target window in i3viswiz it will result in some containers will never be on the cycle path. And as i see it to fix this we need to figure out what container(s) is in the four corners, then make sure jump to the correct corner container when approriate, f.i. if container a is splitv we need to target top left corner when going from B to A, but if we go from C to A in this case we want to target the bottom container (ie normal move/focus up). It is all completely doable, but it will take a more time than i was hoping for, but i will do it.. meanwhile here is updated script with i3fyra and i3viswiz for moving and focusing:

#!/bin/bash

# usage:   cycla [CW|CC] [focus|swap|move]
# default: cycla CW focus

declare -g  rotation action current_container target
declare -Ag i3list layout directions

eval "$(i3list)"

rotation=${1:-CW}
action=${2:-focus}

current_container=${i3list[AWP]}

if [[ ${rotation^^} = CW ]]; then
  layout=(
    [A]=C [B]=A
    [C]=D [D]=B
  )
else # CC
  layout=(
    [A]=B [B]=D
    [C]=A [D]=C
  )
fi

number_of_visibles=${#i3list[LVI]}

case "$number_of_visibles" in
  4 ) target=${layout[$current_container]} ;;
  2 ) target=${i3list[LVI]//$current_container/} ;;
  3 )
    target=${layout[$current_container]}

    # cousin (AFC) is always second choice
    [[ ${i3list[LVI]} =~ $target ]] \
      || target=${i3list[AFC]}
  ;;
  1 ) echo we are one, no swapping ;;
  * ) echo "broken layout"         ;;
esac

[[ $target ]] || exit

directions=(
  [AB]=r
  [AC]=d
  [AD]=r
  [BA]=l
  [BC]=l
  [BD]=d
  [CA]=u
  [CB]=r
  [CD]=r
  [DA]=l
  [DB]=u
  [DC]=l
)

case "$action" in
  focus ) 
    i3viswiz "${directions[${current_container}${target}]}" ;;
  move  )
    i3fyra --move "${directions[${current_container}${target}]}" ;;
  swap  )
    if [[ ${i3list[AFF]} =~ $target ]]
      then i3fyra --swap sibling
      else i3fyra --swap family
    fi
  ;;
esac


dang it, i was hoping that using i3viswiz and i3fyra to focus and move would automatically work even for containers that have splith/v layouts, and it kind of do work but because i have been so smarty pants about how to find the target window in i3viswiz it will result in some containers will never be on the cycle path. And as i see it to fix this we need to figure out what container(s) is in the four corners, then make sure jump to the correct corner container when approriate, f.i. if container a is splitv we need to target top left corner when going from B to A, but if we go from C to A in this case we want to target the bottom container (ie normal move/focus up). It is all completely doable, but it will take a more time than i was hoping for, but i will do it.. meanwhile here is updated script with i3fyra and i3viswiz for moving and focusing: ```bash #!/bin/bash # usage: cycla [CW|CC] [focus|swap|move] # default: cycla CW focus declare -g rotation action current_container target declare -Ag i3list layout directions eval "$(i3list)" rotation=${1:-CW} action=${2:-focus} current_container=${i3list[AWP]} if [[ ${rotation^^} = CW ]]; then layout=( [A]=C [B]=A [C]=D [D]=B ) else # CC layout=( [A]=B [B]=D [C]=A [D]=C ) fi number_of_visibles=${#i3list[LVI]} case "$number_of_visibles" in 4 ) target=${layout[$current_container]} ;; 2 ) target=${i3list[LVI]//$current_container/} ;; 3 ) target=${layout[$current_container]} # cousin (AFC) is always second choice [[ ${i3list[LVI]} =~ $target ]] \ || target=${i3list[AFC]} ;; 1 ) echo we are one, no swapping ;; * ) echo "broken layout" ;; esac [[ $target ]] || exit directions=( [AB]=r [AC]=d [AD]=r [BA]=l [BC]=l [BD]=d [CA]=u [CB]=r [CD]=r [DA]=l [DB]=u [DC]=l ) case "$action" in focus ) i3viswiz "${directions[${current_container}${target}]}" ;; move ) i3fyra --move "${directions[${current_container}${target}]}" ;; swap ) if [[ ${i3list[AFF]} =~ $target ]] then i3fyra --swap sibling else i3fyra --swap family fi ;; esac ```
budRich commented 2025-12-28 13:13:48 +00:00 (Migrated from github.com)

See screenshot below, black rect is approximately the focus path, notice how top left and top right window is never visited.
Image

See screenshot below, black rect is approximately the focus path, notice how top left and top right window is never visited. <img width="1920" height="1080" alt="Image" src="https://github.com/user-attachments/assets/583f35a9-37ac-43ae-9539-ef82f9841284" />
budRich commented 2025-12-28 13:28:46 +00:00 (Migrated from github.com)

i just realized that if i have to do this, get corner windows, i might as well make a semi generic version that works without i3fyra. see, as long as a window is adjacent to the workspace border, we can figure out what direction CC and CW is, with the caveat that we could end up in a crazy layout where the path would get lost. in theory this could happen in i3fyra as well, if a container nests more than one layout, i will just assume that is never the case, but i am aware..

i just realized that if i have to do this, get corner windows, i might as well make a semi generic version that works without i3fyra. see, as long as a window is adjacent to the workspace border, we can figure out what direction CC and CW is, with the caveat that we could end up in a crazy layout where the path would get lost. in theory this could happen in i3fyra as well, if a container nests more than one layout, i will just assume that is never the case, but i am aware..
budRich commented 2025-12-28 23:39:25 +00:00 (Migrated from github.com)

Ok i finally nailed it, this got really hairy, i needed a way to get first and last container in each fyra group, so i added that to i3list. (i first tried adding it properly in i3viswiz, but it was easier to do in i3list).


#!/bin/bash

# usage:   cycla [CW|CC] [focus|swap|move]
# default: cycla CW focus

declare -g  rotation action current_container target
declare -Ag i3list layout directions

eval "$(i3list)"

rotation=${1:-CW}
rotation=${rotation^^}
action=${2:-focus}

current_container=${i3list[AWP]}

if [[ ${rotation^^} = CC ]]; then
  layout=(
    [A]=C [B]=A
    [C]=D [D]=B
  )
else # CC
  layout=(
    [A]=B [B]=D
    [C]=A [D]=C
  )
fi

number_of_visibles=${#i3list[LVI]}

case "$number_of_visibles" in
  4 ) target=${layout[$current_container]} ;;
  2 ) target=${i3list[LVI]//$current_container/} ;;
  3 )
    target=${layout[$current_container]}

    # cousin (AFC) is always second choice
    [[ ${i3list[LVI]} =~ $target ]] \
      || target=${i3list[AFC]}
  ;;
  1 ) echo we are one, no swapping ;;
  * ) echo "broken layout"         ;;
esac

[[ $target ]] || exit

[[ $action = swap ]] && {

  if [[ ${i3list[AFF]} =~ $target ]]
    then i3fyra --swap sibling
    else i3fyra --swap family
  fi

  exit
}

directions=(
  [AB]=r [BA]=l [CA]=u [DA]=l
  [AC]=d [BC]=l [CB]=r [DB]=u
  [AD]=r [BD]=d [CD]=r [DC]=l
)

direction=${directions[${current_container}${target}]}

src_layout=${i3list[C${current_container}L]}
trg_layout=${i3list[C${target}L]}
src_con=${i3list[AWC]}
src_first=${i3list[CF${current_container}]}
src_last=${i3list[CL${current_container}]}
trg_first=${i3list[CF${target}]}
trg_last=${i3list[CL${target}]}

notify-send "$rotation $trg_layout $direction"

case "$direction" in
  l)
    # B -> A
    if [[ $src_layout = splitv && $rotation = CC && $src_con != $src_first ]]; then
      trg_con=$(i3viswiz --parent=LIST --debug-format "%v" --debug "trgcon" up)
    # D -> C
    elif [[ $src_layout = splitv && $rotation = CW && $src_con != $src_last ]]; then
      trg_con=$(i3viswiz --parent=LIST --debug-format "%v" --debug "trgcon" down)
    # move left in own splith till we are first container
    elif [[ $src_layout = splith && $src_con != $src_first ]]; then
      trg_con=$(i3viswiz --parent=LIST --debug-format "%v" --debug "trgcon" left)
    # if target is split h, we always enter at last child
    elif [[ $trg_layout = splith ]]; then
      trg_con=$trg_last
    # B -> A
    elif [[ $trg_layout = splitv && $rotation = CC ]]; then
      trg_con=$trg_first
      corner_case=top-left
    # D -> C
    elif [[ $trg_layout = splitv && $rotation = CW ]]; then
      trg_con=$trg_last
      corner_case=bottom-left
    else
      trg_con=${i3list[C${target}F]}
    fi 
    ;;
  r) 
    # C -> D
    if [[ $src_layout = splitv && $rotation = CC && $src_con != $src_last ]]; then
      trg_con=$(i3viswiz --parent=LIST --debug-format "%v" --debug "trgcon" down)
    # A -> B
    elif [[ $src_layout = splitv && $rotation = CW && $src_con != $src_first ]]; then
      trg_con=$(i3viswiz --parent=LIST --debug-format "%v" --debug "trgcon" up)
    # move right in own splith till we are last container
    elif [[ $src_layout = splith && $src_con != $src_last ]]; then
      trg_con=$(i3viswiz --parent=LIST --debug-format "%v" --debug "trgcon" right)
    # if target is split h, we always enter at first child
    elif [[ $trg_layout = splith ]]; then
      trg_con=$trg_first
    # C -> D
    elif [[ $trg_layout = splitv && $rotation = CC ]]; then
      trg_con=$trg_last
      corner_case=bottom-right
    # A -> B
    elif [[ $trg_layout = splitv && $rotation = CW ]]; then
      trg_con=$trg_first
      corner_case=top-right
    else
      trg_con=${i3list[C${target}F]}
    fi 
    ;;
  u) 
    # D -> B
    if [[ $src_layout = splith && $rotation = CC && $src_con != $src_last ]]; then
      trg_con=$(i3viswiz --parent=LIST --debug-format "%v" --debug "trgcon" right)
    # C -> A
    elif [[ $src_layout = splith && $rotation = CW && $src_con != $src_first ]]; then
      trg_con=$(i3viswiz --parent=LIST --debug-format "%v" --debug "trgcon" left)
    # move up in own splitv till we are first container
    elif [[ $src_layout = splitv && $src_con != $src_first ]]; then
      trg_con=$(i3viswiz --parent=LIST --debug-format "%v" --debug "trgcon" up)
    # if target is splitv, we always enter at last child
    elif [[ $trg_layout = splitv ]]; then
      trg_con=$trg_last
    # D -> B
    elif [[ $trg_layout = splith && $rotation = CC ]]; then
      trg_con=$trg_last
      corner_case=top-right
    # C -> A
    elif [[ $trg_layout = splith && $rotation = CW ]]; then
      trg_con=$trg_first
      corner_case=top-left
    else
      trg_con=${i3list[C${target}F]}
    fi 
    ;;
  d) 
    # A -> C
    if [[ $src_layout = splith && $rotation = CC && $src_con != $src_first ]]; then
      trg_con=$(i3viswiz --parent=LIST --debug-format "%v" --debug "trgcon" left)
    # B -> D
    elif [[ $src_layout = splith && $rotation = CW && $src_con != $src_last ]]; then
      trg_con=$(i3viswiz --parent=LIST --debug-format "%v" --debug "trgcon" right)
    # move down in own splitv till we are last container
    elif [[ $src_layout = splitv && $src_con != $src_last ]]; then
      trg_con=$(i3viswiz --parent=LIST --debug-format "%v" --debug "trgcon" down)
    # if target is splitv, we always enter at first child
    elif [[ $trg_layout = splitv ]]; then
      trg_con=$trg_first
    # A -> C
    elif [[ $trg_layout = splith && $rotation = CC ]]; then
      trg_con=$trg_first
      corner_case=bottom-left
    # B -> D
    elif [[ $trg_layout = splith && $rotation = CW ]]; then
      trg_con=$trg_last
      corner_case=bottom-right
    else
      trg_con=${i3list[C${target}F]}
    fi 
    ;;
esac

i3-msg "[con_id=$trg_con] focus"

God damn it, i just found a new issue, above script works perfectly as long as all four containers are visible, but if one is missing it gets weird. i don't know man, i guess i can bruteforce it for 3 and possibly 2 (it probably gets weird here as well if both layouts are not tabbed or stacked). As you can see every single movement and layout combination is its own corner case, this is getting way too hairy for my liking.

One solution, could be to hack i3viswiz so we can offset the search point. As it is now, if you search left the vertical search point will be at the center of the container, what we want is for the search point to be more or less at the extreme top or bottom (or left or right), yeah, that could work i will give it a try tomorrow.

Ok i finally nailed it, this got really hairy, i needed a way to get first and last container in each fyra group, so i added that to i3list. (i first tried adding it properly in i3viswiz, but it was easier to do in i3list). ``` bash #!/bin/bash # usage: cycla [CW|CC] [focus|swap|move] # default: cycla CW focus declare -g rotation action current_container target declare -Ag i3list layout directions eval "$(i3list)" rotation=${1:-CW} rotation=${rotation^^} action=${2:-focus} current_container=${i3list[AWP]} if [[ ${rotation^^} = CC ]]; then layout=( [A]=C [B]=A [C]=D [D]=B ) else # CC layout=( [A]=B [B]=D [C]=A [D]=C ) fi number_of_visibles=${#i3list[LVI]} case "$number_of_visibles" in 4 ) target=${layout[$current_container]} ;; 2 ) target=${i3list[LVI]//$current_container/} ;; 3 ) target=${layout[$current_container]} # cousin (AFC) is always second choice [[ ${i3list[LVI]} =~ $target ]] \ || target=${i3list[AFC]} ;; 1 ) echo we are one, no swapping ;; * ) echo "broken layout" ;; esac [[ $target ]] || exit [[ $action = swap ]] && { if [[ ${i3list[AFF]} =~ $target ]] then i3fyra --swap sibling else i3fyra --swap family fi exit } directions=( [AB]=r [BA]=l [CA]=u [DA]=l [AC]=d [BC]=l [CB]=r [DB]=u [AD]=r [BD]=d [CD]=r [DC]=l ) direction=${directions[${current_container}${target}]} src_layout=${i3list[C${current_container}L]} trg_layout=${i3list[C${target}L]} src_con=${i3list[AWC]} src_first=${i3list[CF${current_container}]} src_last=${i3list[CL${current_container}]} trg_first=${i3list[CF${target}]} trg_last=${i3list[CL${target}]} notify-send "$rotation $trg_layout $direction" case "$direction" in l) # B -> A if [[ $src_layout = splitv && $rotation = CC && $src_con != $src_first ]]; then trg_con=$(i3viswiz --parent=LIST --debug-format "%v" --debug "trgcon" up) # D -> C elif [[ $src_layout = splitv && $rotation = CW && $src_con != $src_last ]]; then trg_con=$(i3viswiz --parent=LIST --debug-format "%v" --debug "trgcon" down) # move left in own splith till we are first container elif [[ $src_layout = splith && $src_con != $src_first ]]; then trg_con=$(i3viswiz --parent=LIST --debug-format "%v" --debug "trgcon" left) # if target is split h, we always enter at last child elif [[ $trg_layout = splith ]]; then trg_con=$trg_last # B -> A elif [[ $trg_layout = splitv && $rotation = CC ]]; then trg_con=$trg_first corner_case=top-left # D -> C elif [[ $trg_layout = splitv && $rotation = CW ]]; then trg_con=$trg_last corner_case=bottom-left else trg_con=${i3list[C${target}F]} fi ;; r) # C -> D if [[ $src_layout = splitv && $rotation = CC && $src_con != $src_last ]]; then trg_con=$(i3viswiz --parent=LIST --debug-format "%v" --debug "trgcon" down) # A -> B elif [[ $src_layout = splitv && $rotation = CW && $src_con != $src_first ]]; then trg_con=$(i3viswiz --parent=LIST --debug-format "%v" --debug "trgcon" up) # move right in own splith till we are last container elif [[ $src_layout = splith && $src_con != $src_last ]]; then trg_con=$(i3viswiz --parent=LIST --debug-format "%v" --debug "trgcon" right) # if target is split h, we always enter at first child elif [[ $trg_layout = splith ]]; then trg_con=$trg_first # C -> D elif [[ $trg_layout = splitv && $rotation = CC ]]; then trg_con=$trg_last corner_case=bottom-right # A -> B elif [[ $trg_layout = splitv && $rotation = CW ]]; then trg_con=$trg_first corner_case=top-right else trg_con=${i3list[C${target}F]} fi ;; u) # D -> B if [[ $src_layout = splith && $rotation = CC && $src_con != $src_last ]]; then trg_con=$(i3viswiz --parent=LIST --debug-format "%v" --debug "trgcon" right) # C -> A elif [[ $src_layout = splith && $rotation = CW && $src_con != $src_first ]]; then trg_con=$(i3viswiz --parent=LIST --debug-format "%v" --debug "trgcon" left) # move up in own splitv till we are first container elif [[ $src_layout = splitv && $src_con != $src_first ]]; then trg_con=$(i3viswiz --parent=LIST --debug-format "%v" --debug "trgcon" up) # if target is splitv, we always enter at last child elif [[ $trg_layout = splitv ]]; then trg_con=$trg_last # D -> B elif [[ $trg_layout = splith && $rotation = CC ]]; then trg_con=$trg_last corner_case=top-right # C -> A elif [[ $trg_layout = splith && $rotation = CW ]]; then trg_con=$trg_first corner_case=top-left else trg_con=${i3list[C${target}F]} fi ;; d) # A -> C if [[ $src_layout = splith && $rotation = CC && $src_con != $src_first ]]; then trg_con=$(i3viswiz --parent=LIST --debug-format "%v" --debug "trgcon" left) # B -> D elif [[ $src_layout = splith && $rotation = CW && $src_con != $src_last ]]; then trg_con=$(i3viswiz --parent=LIST --debug-format "%v" --debug "trgcon" right) # move down in own splitv till we are last container elif [[ $src_layout = splitv && $src_con != $src_last ]]; then trg_con=$(i3viswiz --parent=LIST --debug-format "%v" --debug "trgcon" down) # if target is splitv, we always enter at first child elif [[ $trg_layout = splitv ]]; then trg_con=$trg_first # A -> C elif [[ $trg_layout = splith && $rotation = CC ]]; then trg_con=$trg_first corner_case=bottom-left # B -> D elif [[ $trg_layout = splith && $rotation = CW ]]; then trg_con=$trg_last corner_case=bottom-right else trg_con=${i3list[C${target}F]} fi ;; esac i3-msg "[con_id=$trg_con] focus" ``` God damn it, i just found a new issue, above script works perfectly as long as all four containers are visible, but if one is missing it gets weird. i don't know man, i guess i can bruteforce it for 3 and possibly 2 (it probably gets weird here as well if both layouts are not tabbed or stacked). As you can see every single movement and layout combination is its own corner case, this is getting way too hairy for my liking. One solution, could be to hack i3viswiz so we can offset the search point. As it is now, if you search left the vertical search point will be at the center of the container, what we want is for the search point to be more or less at the extreme top or bottom (or left or right), yeah, that could work i will give it a try tomorrow.
budRich commented 2025-12-29 15:08:07 +00:00 (Migrated from github.com)

ok, i hacked viswiz to allow for offsetting search pos, and it worked. then i realized that the internal last focused window pathfinding shenanigans was what was breaking layouts where we only had three fyra containers. so i added an option to ignore pathfinding shenanigans, and it worked. But now i found the last corner case, if we only have two fyra containers, say C and A, with a horizontal main orientation, and one or more of C and A has splith layout, we end up in a impossible scenario i think:

imagine C is splith, each C is a visible window, A only have one visible window

C C C C A
1 2 3 4

if direction is CW and we are in C1, we will go right till we end up in A, all good. CW A will turn direction to right, going to C4, but now you see, CW from C4 should according to all logic focus A which it does, but the hidden purpose of this cycling command is that we can visit all container, but no matter how you look at it from this point it gets sketchy. One "solution" would be that we now when we go CW from A -> C4, for the next CW trigger, we change CW to CC so we keep going right till we end up in A, but here we get the same problem, no CC will just go from A -> C1 back and forth.

Aha, this is what we should do, if no fyra container has the opposite split as the main split, we force CW to always be right (or down if mainsplit is splitv), and CC to always be left (or up), that should cycle all visible containers, because we viswiz wraps at workspace, if user hasn't configured, but that can probably be bypassed for this. yes, thats what i need to do.

ok, i hacked viswiz to allow for offsetting search pos, and it worked. then i realized that the internal last focused window pathfinding shenanigans was what was breaking layouts where we only had three fyra containers. so i added an option to ignore pathfinding shenanigans, and it worked. But now i found the last corner case, if we only have two fyra containers, say C and A, with a horizontal main orientation, and one or more of C and A has splith layout, we end up in a impossible scenario i think: ``` imagine C is splith, each C is a visible window, A only have one visible window C C C C A 1 2 3 4 ``` if direction is CW and we are in C1, we will go right till we end up in A, all good. CW A will turn direction to right, going to C4, but now you see, CW from C4 should according to all logic focus A which it does, but the hidden purpose of this cycling command is that we can visit all container, but no matter how you look at it from this point it gets sketchy. One "solution" would be that we now when we go CW from A -> C4, for the next CW trigger, we change CW to CC so we keep going right till we end up in A, but here we get the same problem, no CC will just go from A -> C1 back and forth. Aha, this is what we should do, if no fyra container has the opposite split as the main split, we force CW to always be right (or down if mainsplit is splitv), and CC to always be left (or up), that should cycle all visible containers, because we viswiz wraps at workspace, if user hasn't configured, but that can probably be bypassed for this. yes, thats what i need to do.
budRich commented 2025-12-29 16:31:39 +00:00 (Migrated from github.com)

Dear diary, i finally got the issue described above solved. And found a bug in the shared main.awk script, that didn't store output values properly so workspace focus wrapping has not worked at all since that bug was introduced.. but i fixed it. I know stumbled upon another corner case, see screenshot below:

Image
Dear diary, i finally got the issue described above solved. And found a bug in the shared main.awk script, that didn't store output values properly so workspace focus wrapping has not worked at all since that bug was introduced.. but i fixed it. I know stumbled upon another corner case, see screenshot below: <img width="1920" height="1080" alt="Image" src="https://github.com/user-attachments/assets/0b30440a-0538-41ea-a8be-0885a5177fe2" />
budRich commented 2025-12-29 21:43:28 +00:00 (Migrated from github.com)

there are actually two corner cases issues that can happen when we have 3 fyra containers and one (or more) of them has the same split as the main, in this case splith. Here is screenshot with the short circuit highlighted:

Image

The leftmost, is only between two containers, and it is possible by simply inverting the direction if next focus is going to same container as preiously focused, easy to get needed info from i3viswiz. The right most short circuit is a bit more involved, but i managed to write a test that catches that as well, so that when last container of C has focus, instead of taking the short cut to D (or B depending on rotation), we revert direction, meaning second to last container of C now has focus, it feels good. but, the direction we want to go is still to the right. So i thought, if i keep the test for if previously focused is the same as next to be focused and invert direction if that is the case, it should work. And it does work! but, that means that now we cannot change rotation, changing rotation is literally to focus the last container that had focus. I can limit that to only apply when a container has the same split as main, and only when 3 fyra containers are visible, but it is imo not good enough. I am out of ideas for today i think...

there are actually two corner cases issues that can happen when we have 3 fyra containers and one (or more) of them has the same split as the main, in this case splith. Here is screenshot with the short circuit highlighted: <img width="1920" height="1080" alt="Image" src="https://github.com/user-attachments/assets/23455a3d-9253-4615-853e-c9fb9f05f8e5" /> The leftmost, is only between two containers, and it is possible by simply inverting the direction if next focus is going to same container as preiously focused, easy to get needed info from i3viswiz. The right most short circuit is a bit more involved, but i managed to write a test that catches that as well, so that when last container of C has focus, instead of taking the short cut to D (or B depending on rotation), we revert direction, meaning second to last container of C now has focus, it feels good. but, the direction we want to go is still to the right. So i thought, if i keep the test for if previously focused is the same as next to be focused and invert direction if that is the case, it should work. And it does work! but, that means that now we cannot change rotation, changing rotation is literally to focus the last container that had focus. I can limit that to only apply when a container has the same split as main, and only when 3 fyra containers are visible, but it is imo not good enough. I am out of ideas for today i think...
budRich commented 2025-12-29 21:58:50 +00:00 (Migrated from github.com)

the problem is really that when a container like C in the image is vertically maximized, it is more or less impossible to know if clockwise is left or right, now i simply use the container name, so in C (which is the bottom container) clockwise is left, and A CW is right. The blue arrow, a counter clockwise CC rotation: B -> C (second choice since A is missing), C -> D , since it is splith there is a test that will make sure we are at the last container (ie we have completed C) before proceeding, that test passes and we move to D, D -> B. hmm, i guess if we inverted direction when B -> C so the instead of going left we go right, which would warp to the first container of C would solve everything.

hmm, how to express that test.
If target containers layout is the same as main split, and target is first or last. no this will not work, we have to do something like this. target container is same as main, and target container is last and direction is left (or up on mainsplit=spliv), we focus the first container instead of the last. same logic applies if direction is right, and target is first in a splith container. we can't simply invert the direction, incase our own layout is splith, yeah, that will do it.

the problem is really that when a container like C in the image is vertically maximized, it is more or less impossible to know if clockwise is left or right, now i simply use the container name, so in C (which is the bottom container) clockwise is left, and A CW is right. The blue arrow, a counter clockwise CC rotation: B -> C (second choice since A is missing), C -> D , since it is splith there is a test that will make sure we are at the last container (ie we have completed C) before proceeding, that test passes and we move to D, D -> B. hmm, i guess if we inverted direction when B -> C so the instead of going left we go right, which would warp to the first container of C would solve everything. hmm, how to express that test. If target containers layout is the same as main split, and target is first or last. no this will not work, we have to do something like this. target container is same as main, and target container is last and direction is left (or up on mainsplit=spliv), we focus the first container instead of the last. same logic applies if direction is right, and target is first in a splith container. we can't simply invert the direction, incase our own layout is splith, yeah, that will do it.
budRich commented 2025-12-30 20:07:36 +00:00 (Migrated from github.com)

I finally got it working. When i fixed the green arrow shortcut thing it introduced a new edge case, imagine a CC movement warping C1 -> B, but now imagine B also being splith, so we would warp to B(last), if we now do CC again, target container is D (direction down) but since we are in a hsplit we first need to "complete" it i.e. be in the last before we can move to D, but we are in last C, so we jump to D, then Last C, and never visit the earlier containers in B...

I had to add yet more tests to catch this problem, and when i now sit here with the final script i realize that it is kind of crap, it is sweet as long as there are only tabbed and maybe one split container, but as soon as layout is a bit more crazy with two or more non tabbed/stacked layouts it just feels jank, especially now with all this edge case warping so focus doesn't flow naturally at all...

Meanwhile focusing in directions u|l|r|d works exactly as expected, and now flawlessly. I actually found yet another sneaky bug in the viswiz algorithm for finding windows that prevented warping of focus within a workspace vertically.

So I have decided to scrap this whole feature from i3ass, i will polish the script a bit and publish it in the forum (discussion tab on github) .

But the cycle swap, felt nice, is more or less fool proof and simple. I will add it as an option to i3fyra swap, i3fyra --swap cycle .

I finally got it working. When i fixed the green arrow shortcut thing it introduced a new edge case, imagine a CC movement warping C1 -> B, but now imagine B also being splith, so we would warp to B(last), if we now do CC again, target container is D (direction down) but since we are in a hsplit we first need to "complete" it i.e. be in the last before we can move to D, but we are in last C, so we jump to D, then Last C, and never visit the earlier containers in B... I had to add yet more tests to catch this problem, and when i now sit here with the final script i realize that it is kind of crap, it is sweet as long as there are only tabbed and maybe one split container, but as soon as layout is a bit more crazy with two or more non tabbed/stacked layouts it just feels jank, especially now with all this edge case warping so focus doesn't flow naturally at all... Meanwhile focusing in directions u|l|r|d works exactly as expected, and now flawlessly. I actually found yet another sneaky bug in the viswiz algorithm for finding windows that prevented warping of focus within a workspace vertically. So I have decided to scrap this whole feature from i3ass, i will polish the script a bit and publish it in the forum (discussion tab on github) . But the cycle swap, felt nice, is more or less fool proof and simple. I will add it as an option to i3fyra swap, `i3fyra --swap cycle` .
budRich commented 2025-12-30 20:17:22 +00:00 (Migrated from github.com)

or here is the "final" script in case i never polish and pub in disucssions:

#!/bin/bash

# usage:   cycla [CW|CC] [focus|swap|move]
# default: cycla CW focus

declare -g  rotation action current_container target
declare -Ag i3list layout directions

eval "$(i3list)"

rotation=${1:-CW}
rotation=${rotation^^}
action=${2:-focus}

current_container=${i3list[AWP]}

if [[ ${rotation^^} = CC ]]; then
  layout=(
    [A]=C [B]=A
    [C]=D [D]=B
  )
else # CW
  layout=(
    [A]=B [B]=D
    [C]=A [D]=C
  )
fi

number_of_visibles=${#i3list[LVI]}

case "$number_of_visibles" in
  4 ) target=${layout[$current_container]} ;;
  2 ) target=${i3list[LVI]//$current_container/} ;;
  3 )
    target=${layout[$current_container]}

    # cousin (AFC) is always second choice
    [[ ${i3list[LVI]} =~ $target ]] \
      || target=${i3list[AFC]}
  ;;
  1 ) echo we are one, no swapping ;;
  * ) echo "broken layout"         ;;
esac

[[ $target ]] || exit

[[ $action = swap ]] && {
  if [[ ${i3list[AFF]} =~ $target ]]
    then i3fyra --swap sibling
    else i3fyra --swap family
  fi
  exit
}

src_layout=${i3list[C${current_container}L]}
trg_layout=${i3list[C${target}L]}
src_con=${i3list[AWC]}
src_first=${i3list[CF${current_container}]}
src_last=${i3list[CL${current_container}]}
trg_first=${i3list[CF${target}]}
trg_last=${i3list[CL${target}]}
x_offset=50
y_offset=50

if [[ ${i3list[ORI]} = horizontal ]]
  then main_layout=splith
  else main_layout=splitv
fi

# if there exist only two containers and no
# containers with the opposing layout as the main
# split, we need to force direction
if [[ $number_of_visibles = 2 && ${i3list[ORI]} = horizontal \
  && ($trg_layout != splitv && $src_layout != splitv) ]]; then
    if [[ $rotation = CC ]]
      then direction=r
      else direction=l
    fi

elif [[ $number_of_visibles = 2 && ${i3list[ORI]} != horizontal \
  && ($trg_layout != splith && $src_layout != splith) ]]; then
    if [[ $rotation = CC ]]
      then direction=u
      else direction=d
    fi

# we don't jump to target container till we have "completed"
# the one we are in, ie if our own container is splith
# and the direction is not l|r, we first need to get to
# end or start of our own container first.
elif [[ $src_con != "$src_first" && $src_layout = splith && \
    ( ( $rotation = CC && $current_container =~ A|B ) || \
      ( $rotation = CW && $current_container =~ C|D ) ) ]]; then
    direction=l

elif [[ $src_con != "$src_last" && $src_layout = splith && \
      ( ( $rotation = CC && $current_container =~ C|D ) || \
        ( $rotation = CW && $current_container =~ A|B ) ) ]]; then
      direction=r

elif [[ $src_con != "$src_last" && $src_layout = splitv && \
      ( ( $rotation = CC && $current_container =~ A|C ) || \
        ( $rotation = CW && $current_container =~ B|D ) ) ]]; then
      direction=d

elif [[ $src_con != "$src_first" && $src_layout = splitv && \
      ( ( $rotation = CC && $current_container =~ B|D ) || \
        ( $rotation = CW && $current_container =~ A|C ) ) ]]; then
      direction=u

elif [[ ${i3list[ORI]} = horizontal ]]; then
  directions=(
    [AB]=r [BA]=l [CA]=u [DA]=l
    [AC]=d [BC]=l [CB]=r [DB]=u
    [AD]=r [BD]=d [CD]=r [DC]=l 
  )
  direction=${directions[${current_container}${target}]}

elif [[ ${i3list[ORI]} != horizontal ]]; then

  directions=(
    [AB]=r [BA]=l [CA]=u [DA]=u
    [AC]=d [BC]=d [CB]=u [DB]=u
    [AD]=d [BD]=d [CD]=r [DC]=l 
  )
  direction=${directions[${current_container}${target}]}

fi

case "$direction" in
  l|r)
    if [[ ($target =~ C && $rotation = CW) || ($target =~ D && $rotation = CC)  ]]
      then y_offset=100
      else y_offset=1
    fi
    ;;
  u|d)
    if [[ ($target =~ D && $rotation = CW) || ($target =~ B && $rotation = CC)  ]]
      then x_offset=95
      else x_offset=1
    fi
    ;;
esac

read -r target_id target_container < <(
  i3viswiz \
    --instance LIST \
    --debug "trgcon,trgpar" --debug-format "%v " \
    --ignore-last \
    --offset-x "$x_offset" \
    --offset-y "$y_offset" \
    --force-wrap workspace \
    "$direction"
)

# C C C C B
# C C C C D
# ^ ^   ^ ^

# there are some edge case scenarios that only happens
# when we have 3 fyra containers visible. Say for instance
# A is hidden and C is splith, layout of B and D in this case
# doesn't matter but say they are tabbed. The problem is that
# each window in C now is vertically maximized, and deciding
# if CC is left or right gets tricky. Worse is that this also
# Causes two short circuits if there are more than 2 windows in C
# Between the first and second window of C.
# The other is between the last window of C and B and D.

if [[ ${i3list[LVI]} =~ ${i3list[AFS]} ]]; then
  if [[ $current_container = $target_container || $target_container = ${i3list[AFS]} ]]
    then target_sibling_visible=1
    else target_sibling_visible=0
  fi
else
  if [[ $current_container = $target_container || $target_container = ${i3list[AFS]} ]]
    then target_sibling_visible=0
    else target_sibling_visible=1
  fi
fi

if [[ $number_of_visibles = 3 && $target_sibling_visible = 0 ]]; then

  last_focused=$(i3var get i3viswiz-last-direction)

  # by inverting direction in this case we will warp
  # focus to the opposite side of the workspace (ie. first C -> D)
  if [[ $target_id = $last_focused && $target_container = $current_container && ($src_con = $src_first || $src_con = $src_last) && $src_layout = $main_layout ]]; then

    case "$direction" in
      l) direction=r ;;
      r) direction=l ;;
      d) direction=u ;;
      u) direction=d ;;
    esac

    read -r target_id target_container < <(
      i3viswiz \
        --instance LIST \
        --debug "trgcon,trgpar" --debug-format "%v " \
        --ignore-last \
        --offset-x "$x_offset" \
        --offset-y "$y_offset" \
        --force-wrap workspace \
        "$direction"
    )

    # this fix causes another edge, consider this:
    #1 A B
    #2 A 
    #3 C C
    #4 C C

    # if we go CW from C4 we will warp to A1
    # CW again, will go to B, and A2 is never
    # visited.
    # So when we do this invert direction warp
    # we need to check if target has same layout
    # as us, then if new direction is 
    # down|right, we go to new_trg_last
    # up|left, we go to new_trg_first

    # when all is working, path will be:
    # CW: B -> C3 -> C4 -> A2 -> A1 -> B
    # CC: B -> A1 -> A2 -> C3 -> C4 -> B

    [[ ${i3list[C${target_container}L]} = $src_layout ]] && {
      
      if [[ $direction =~ u|l ]]
        then target_id=${i3list[CF${target_container}]}
        else target_id=${i3list[CL${target_container}]}
      fi
    }

  # CBF=93924820074768

  # --------------------------------------------
  
  # target container is same as main, and target
  # container is last and direction is left(or up
  # on mainsplit=spliv), we focus the first
  # container instead of the last. same logic
  # applies if direction is right, and target is
  # first in a splith container. we can't simply
  # invert the direction, incase our own layout
  # is splith, yeah, that will do it.
  elif [[ $target_container != $current_container && $trg_layout = $main_layout && $target != ${i3list[AFT]} ]]; then
    
    if   [[ $target_id = $trg_last  && $trg_layout = splith && $direction = l ]]; then
      target_id=$trg_first
    elif [[ $target_id = $trg_first && $trg_layout = splith && $direction = r ]]; then
      target_id=$trg_last
    elif [[ $target_id = $trg_first && $trg_layout = splitv && $direction = d ]]; then
      target_id=$trg_last
    elif [[ $target_id = $trg_last  && $trg_layout = splitv && $direction = u ]]; then
      target_id=$trg_first
    fi

  fi

fi

case "$action" in
  focus ) 
    # notify-send "$rotation $direction $current_container -> $target"
    # i3viswiz \
    #   --ignore-last \
    #   --offset-x "$x_offset" \
    #   --offset-y "$y_offset" \
    #   --force-wrap workspace \
    #   "$direction"
    i3-msg "[con_id=$target_id] focus"

    # TODO: if we instead focus directly with viswiz
    #       we don't need to manually set this mark..
    i3var set i3viswiz-last-direction "$src_con"

    ;;
  move  )
    : ;;
    # i3fyra --move "${directions[${current_container}${target}]}" ;;
esac

it relies on some fixes in i3ass not yet commited, but i will commit.

or here is the "final" script in case i never polish and pub in disucssions: ``` bash #!/bin/bash # usage: cycla [CW|CC] [focus|swap|move] # default: cycla CW focus declare -g rotation action current_container target declare -Ag i3list layout directions eval "$(i3list)" rotation=${1:-CW} rotation=${rotation^^} action=${2:-focus} current_container=${i3list[AWP]} if [[ ${rotation^^} = CC ]]; then layout=( [A]=C [B]=A [C]=D [D]=B ) else # CW layout=( [A]=B [B]=D [C]=A [D]=C ) fi number_of_visibles=${#i3list[LVI]} case "$number_of_visibles" in 4 ) target=${layout[$current_container]} ;; 2 ) target=${i3list[LVI]//$current_container/} ;; 3 ) target=${layout[$current_container]} # cousin (AFC) is always second choice [[ ${i3list[LVI]} =~ $target ]] \ || target=${i3list[AFC]} ;; 1 ) echo we are one, no swapping ;; * ) echo "broken layout" ;; esac [[ $target ]] || exit [[ $action = swap ]] && { if [[ ${i3list[AFF]} =~ $target ]] then i3fyra --swap sibling else i3fyra --swap family fi exit } src_layout=${i3list[C${current_container}L]} trg_layout=${i3list[C${target}L]} src_con=${i3list[AWC]} src_first=${i3list[CF${current_container}]} src_last=${i3list[CL${current_container}]} trg_first=${i3list[CF${target}]} trg_last=${i3list[CL${target}]} x_offset=50 y_offset=50 if [[ ${i3list[ORI]} = horizontal ]] then main_layout=splith else main_layout=splitv fi # if there exist only two containers and no # containers with the opposing layout as the main # split, we need to force direction if [[ $number_of_visibles = 2 && ${i3list[ORI]} = horizontal \ && ($trg_layout != splitv && $src_layout != splitv) ]]; then if [[ $rotation = CC ]] then direction=r else direction=l fi elif [[ $number_of_visibles = 2 && ${i3list[ORI]} != horizontal \ && ($trg_layout != splith && $src_layout != splith) ]]; then if [[ $rotation = CC ]] then direction=u else direction=d fi # we don't jump to target container till we have "completed" # the one we are in, ie if our own container is splith # and the direction is not l|r, we first need to get to # end or start of our own container first. elif [[ $src_con != "$src_first" && $src_layout = splith && \ ( ( $rotation = CC && $current_container =~ A|B ) || \ ( $rotation = CW && $current_container =~ C|D ) ) ]]; then direction=l elif [[ $src_con != "$src_last" && $src_layout = splith && \ ( ( $rotation = CC && $current_container =~ C|D ) || \ ( $rotation = CW && $current_container =~ A|B ) ) ]]; then direction=r elif [[ $src_con != "$src_last" && $src_layout = splitv && \ ( ( $rotation = CC && $current_container =~ A|C ) || \ ( $rotation = CW && $current_container =~ B|D ) ) ]]; then direction=d elif [[ $src_con != "$src_first" && $src_layout = splitv && \ ( ( $rotation = CC && $current_container =~ B|D ) || \ ( $rotation = CW && $current_container =~ A|C ) ) ]]; then direction=u elif [[ ${i3list[ORI]} = horizontal ]]; then directions=( [AB]=r [BA]=l [CA]=u [DA]=l [AC]=d [BC]=l [CB]=r [DB]=u [AD]=r [BD]=d [CD]=r [DC]=l ) direction=${directions[${current_container}${target}]} elif [[ ${i3list[ORI]} != horizontal ]]; then directions=( [AB]=r [BA]=l [CA]=u [DA]=u [AC]=d [BC]=d [CB]=u [DB]=u [AD]=d [BD]=d [CD]=r [DC]=l ) direction=${directions[${current_container}${target}]} fi case "$direction" in l|r) if [[ ($target =~ C && $rotation = CW) || ($target =~ D && $rotation = CC) ]] then y_offset=100 else y_offset=1 fi ;; u|d) if [[ ($target =~ D && $rotation = CW) || ($target =~ B && $rotation = CC) ]] then x_offset=95 else x_offset=1 fi ;; esac read -r target_id target_container < <( i3viswiz \ --instance LIST \ --debug "trgcon,trgpar" --debug-format "%v " \ --ignore-last \ --offset-x "$x_offset" \ --offset-y "$y_offset" \ --force-wrap workspace \ "$direction" ) # C C C C B # C C C C D # ^ ^ ^ ^ # there are some edge case scenarios that only happens # when we have 3 fyra containers visible. Say for instance # A is hidden and C is splith, layout of B and D in this case # doesn't matter but say they are tabbed. The problem is that # each window in C now is vertically maximized, and deciding # if CC is left or right gets tricky. Worse is that this also # Causes two short circuits if there are more than 2 windows in C # Between the first and second window of C. # The other is between the last window of C and B and D. if [[ ${i3list[LVI]} =~ ${i3list[AFS]} ]]; then if [[ $current_container = $target_container || $target_container = ${i3list[AFS]} ]] then target_sibling_visible=1 else target_sibling_visible=0 fi else if [[ $current_container = $target_container || $target_container = ${i3list[AFS]} ]] then target_sibling_visible=0 else target_sibling_visible=1 fi fi if [[ $number_of_visibles = 3 && $target_sibling_visible = 0 ]]; then last_focused=$(i3var get i3viswiz-last-direction) # by inverting direction in this case we will warp # focus to the opposite side of the workspace (ie. first C -> D) if [[ $target_id = $last_focused && $target_container = $current_container && ($src_con = $src_first || $src_con = $src_last) && $src_layout = $main_layout ]]; then case "$direction" in l) direction=r ;; r) direction=l ;; d) direction=u ;; u) direction=d ;; esac read -r target_id target_container < <( i3viswiz \ --instance LIST \ --debug "trgcon,trgpar" --debug-format "%v " \ --ignore-last \ --offset-x "$x_offset" \ --offset-y "$y_offset" \ --force-wrap workspace \ "$direction" ) # this fix causes another edge, consider this: #1 A B #2 A #3 C C #4 C C # if we go CW from C4 we will warp to A1 # CW again, will go to B, and A2 is never # visited. # So when we do this invert direction warp # we need to check if target has same layout # as us, then if new direction is # down|right, we go to new_trg_last # up|left, we go to new_trg_first # when all is working, path will be: # CW: B -> C3 -> C4 -> A2 -> A1 -> B # CC: B -> A1 -> A2 -> C3 -> C4 -> B [[ ${i3list[C${target_container}L]} = $src_layout ]] && { if [[ $direction =~ u|l ]] then target_id=${i3list[CF${target_container}]} else target_id=${i3list[CL${target_container}]} fi } # CBF=93924820074768 # -------------------------------------------- # target container is same as main, and target # container is last and direction is left(or up # on mainsplit=spliv), we focus the first # container instead of the last. same logic # applies if direction is right, and target is # first in a splith container. we can't simply # invert the direction, incase our own layout # is splith, yeah, that will do it. elif [[ $target_container != $current_container && $trg_layout = $main_layout && $target != ${i3list[AFT]} ]]; then if [[ $target_id = $trg_last && $trg_layout = splith && $direction = l ]]; then target_id=$trg_first elif [[ $target_id = $trg_first && $trg_layout = splith && $direction = r ]]; then target_id=$trg_last elif [[ $target_id = $trg_first && $trg_layout = splitv && $direction = d ]]; then target_id=$trg_last elif [[ $target_id = $trg_last && $trg_layout = splitv && $direction = u ]]; then target_id=$trg_first fi fi fi case "$action" in focus ) # notify-send "$rotation $direction $current_container -> $target" # i3viswiz \ # --ignore-last \ # --offset-x "$x_offset" \ # --offset-y "$y_offset" \ # --force-wrap workspace \ # "$direction" i3-msg "[con_id=$target_id] focus" # TODO: if we instead focus directly with viswiz # we don't need to manually set this mark.. i3var set i3viswiz-last-direction "$src_con" ;; move ) : ;; # i3fyra --move "${directions[${current_container}${target}]}" ;; esac ``` it relies on some fixes in i3ass not yet commited, but i will commit.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
bud/i3ass#231
No description provided.