Feature: Implement sticky floating windows
https://i3wm.org/docs/userguide.html#_sticky_floating_windows
- Implement sticky floating windows
- Implement sticky sidebar windows (feature interaction with:
flatten-workspace-tree,close-all-windows-but-current)
Note to myself: review usages of visualWorkspace
todo:
- Make browsers PIP windows sticky by default, once this is implemented
- Make Outlook reminder window sticky by default https://github.com/nikitabobko/AeroSpace/discussions/1251#discussioncomment-12679986
Is it possible to have sticky non-floating windows? SImilar to this request: https://github.com/koekeishiya/yabai/issues/1843
I'd like to have a constant window on my screen as I flip through workspaces.
"sticky sidebar" windows bring some challenges
- What if users want to have several sticky sidebar windows
- What should happen if a sticky sidebar window is moved with the mouse?
- Should it become a regular tiling window?
- Or should it remain sticky sidebar?
- Should "sticky sidebar" windows be a per-monitor thing?
- If the answer is yes, how users are supposed to move sticky sidebar window between monitors?
- What should happen if users move a window to a monitor where another sticky sidebar windows is already presented?
- If the answer is yes, how users are supposed to move sticky sidebar window between monitors?
But in general, I like the suggestion, I always wanted to have sticky non-floating windows in i3
Maybe we are better off with a feature like "virtual monitors". That way you could place two virtual monitors on your physical monitor. You could cycle workspaces only on one of those virtual monitors
Is there any update on this? I have some widget windows; it would be great if we could keep them on all workspaces.
Maybe we are better off with a feature like "virtual monitors". That way you could place two virtual monitors on your physical monitor. You could cycle workspaces only on one of those virtual monitors
That's indeed a great thing to have. For i3 I typically use xrandr to split up a 5120x? physical device into three virtual monitors, the latter of which can then be used for workspace assignments.
But this does not necessarily solve the issue for floating (possibly workspace-independent) windows (assuming a certain preference for floating Zoom/Teams/etc. because it's easier to re-arrange during meetings). So I'd vote for both :)
Has this ever been implemented? I'm looking for a way to keep FF PiP window in the same position when moving workspaces
For now, I'm manually doing this when I switch workspaces:
- Before I switch; I search to see if there are any PIP windows
- I focus that window and move it to the target workspace (too bad
aerospacedoesn't let me domove-node-to-workspace --window-id <window-id> <target-ws>... so sad 😔 ) - Then, I switch to the target workspace.
I must say, it's a very weird and terrible experience, but at least the PIP window is "sticky"! 😐
Would be super neat to keep a Zoom floating window as sticky so I can see whats going on in a meeting while I cycle through workspaces.
I'm definitely in need of this feature! Hope it comes soon!
Can we extend the functionality of exec-on-workspace-change,force specific windows to move-node-to-{{currently focused workspace}}?
I thought this would be possible to jury-rig for at least one window using exec-on-workspace-change, but I'm not having much success. My attempt, in case anyone can make it work:
exec-on-workspace-change = ['/bin/bash', '-c',
'WIN=$(aerospace list-windows --all | grep "<Window Title>" | cut -d" " -f1); [ -n "$WIN" ] && aerospace move-node-to-workspace --window-id="$WIN" "$AEROSPACE_FOCUSED_WORKSPACE"'
]
@andsnpl: I think the issue with yours is the = after --window-id. This works:
exec-on-workspace-change = ['/bin/bash', '-c',
'WIN=$(aerospace list-windows --all | grep "<Window Title>" | cut -d" " -f1); [ -n "$WIN" ] && aerospace move-node-to-workspace --window-id "$WIN" "$AEROSPACE_FOCUSED_WORKSPACE"'
]
However, it seemed to move the window to the focused workspace but it lost focus so it would be behind my other windows. I tried giving it focus as part of exec-on-workspace-change (see below), which kinda works (moves the window and gives it focus), but it's not really useable for my needs as clicking on any other window will send it to the back:
exec-on-workspace-change = ['/bin/bash', '-c',
'WIN=$(aerospace list-windows --all | grep "<Window Title>" | cut -d" " -f1); [ -n "$WIN" ] && aerospace move-node-to-workspace --window-id "$WIN" "$AEROSPACE_FOCUSED_WORKSPACE" && aerospace focus --window-id "$WIN"'
]
Edit: my bad. aerospace command was not in PATH for bash. It looks like I needed to run bash with --login to pick up the path through /etc/profile. Now I'm just curious why this wasn't an issue for you...
~~Thanks for the feedback @gservat. It still doesn't work for me. I tried echoing the winId to a file:~~
exec-on-workspace-change = ['/bin/bash', '-c',
# 'WIN=$(aerospace list-windows --all | grep "<Window Title>" | cut -d" " -f1); [ -n "$WIN" ] && aerospace move-node-to-workspace --window-id "$WIN" "$AEROSPACE_FOCUSED_WORKSPACE"'
'WIN=$(aerospace list-windows --all | grep "<Window Title>" | cut -d" " -f1); echo "$WIN -> $AEROSPACE_FOCUSED_WORKSPACE" > "$HOME/Desktop/floating_window.txt"'
]
~~but the value printed is empty.~~
-> G
~~This is the case no matter what window title I'm searching for. I wonder if there is something in my config that is causing the nested aerospace invocations to not see the current window list.~~
As far as the focus/always-on-top shortcomings, I can see how that would need some built-in solution. In my case, the window I'm trying to move has that behavior anyway so it's not a problem for me.
@andsnpl: aerospace lives in /opt/homebrew/bin for me. This is added to $PATH by the homebrew init script in my zsh environment. Guessing when bash is loaded by aerospace, it inherits my zsh environment? Not sure why it doesn't work for you though as I'm guessing its in your path too.
@gservat ah, I think that's it. You seem to be on Apple silicon and I'm still on Intel, so homebrew has different install behavior. My aerospace lives in /usr/local/bin, not /opt/homebrew/bin.
If you're using the start-at-login config option in aerospace.toml, then AeroSpace is being started for you by launchd, as it is for me. It's not (unless I'm much mistaken) executing any zsh environment for either of us. Instead what's happening is AeroSpace is manually inserting the /opt/homebrew dir into the path as described here, and probably nobody's realized that that's not good enough for Intel macs.
@andsnpl: ahhh yes, good find! I wonder if it's worth filing a PR to update the default exec env variables to add /usr/local/bin to the path?
I have the following script (defined as on-ws-change.sh and executable) to move Chrome PiP window:
#!/usr/bin/env sh
ws=${1:-$AEROSPACE_FOCUSED_WORKSPACE}
IFS=$'\n' all_wins=$(aerospace list-windows --all --format '%{window-id}|%{app-name}|%{window-title}|%{monitor-id}|%{workspace}')
IFS=$'\n' all_ws=$(aerospace list-workspaces --all --format '%{workspace}|%{monitor-id}')
chrome_pip=$(printf '%s\n' $all_wins | rg 'Picture in Picture')
target_mon=$(printf '%s\n' $all_ws | rg "$ws" | cut -d'|' -f2 | xargs)
move_win() {
local win="$1"
[[ -n $win ]] || return 0
local win_mon=$(printf $win | cut -d'|' -f4 | xargs)
local win_id=$(printf $win | cut -d'|' -f1 | xargs)
local win_app=$(printf $win | cut -d'|' -f2 | xargs)
local win_ws=$(printf $win | cut -d'|' -f5 | xargs)
[[ $target_mon != $win_mon ]] && return 0
[[ $ws == $win_ws ]] && return 0
aerospace move-node-to-workspace --window-id $win_id $ws
}
move_win "${chrome_pip}"
And in my aerospace.toml, I have this:
exec-on-workspace-change = ['<full-path-to>/on-ws-change.sh']
It looks like pip is a very common use case, safari pip seems to work out of box (perhaps it is due to the script I installed to fix zen browser pip?), would be nice if this is buildin instead
@farzadmf Your code breaks when I have multiple monitors connected (1 or 2), and I'm trying to switch to workspace named 1 or 2.
A quick fix would be to anchor the rg regex at the beginning of the string, when calculating the variable target_mon.
target_mon=$(printf '%s\n' $all_ws | rg "^$ws" | cut -d'|' -f2 | xargs)
Adding it here as reference in case there's anyone else trying to troubleshoot this.
Oh cool, thanks for letting me know @thalesmello ; I didn't even think that someone would be using my code 😆
I've updated @farzadmf 's script with support for different pip window titles. I tried to get it to support multple pip windows at once but no luck yet.
#!/usr/bin/env sh
# This seems to only work a single pip window at a time for now
ws=${1:-$AEROSPACE_FOCUSED_WORKSPACE}
IFS=$'\n' all_wins=$(aerospace list-windows --all --format '%{window-id}|%{app-name}|%{window-title}|%{monitor-id}|%{workspace}')
IFS=$'\n' all_ws=$(aerospace list-workspaces --all --format '%{workspace}|%{monitor-id}')
# Array of possible window titles
pip_titles=("Picture-in-picture" "Picture-in-Picture" "Picture in Picture" "Picture in picture")
# Function to find matching PIP windows
find_pip_windows() {
local titles=("$@")
local result=""
for title in "${titles[@]}"; do
local matches=$(printf '%s\n' "$all_wins" | rg "$title")
result="$result"$'\n'"$matches"
done
echo "$result" | sed '/^\s*$/d' # Remove empty lines
}
pip_wins=$(find_pip_windows "${pip_titles[@]}")
target_mon=$(printf '%s\n' "$all_ws" | rg "^$ws" | cut -d'|' -f2 | xargs)
move_win() {
local win="$1"
[[ -n $win ]] || return 0
local win_mon=$(echo "$win" | cut -d'|' -f4 | xargs)
local win_id=$(echo "$win" | cut -d'|' -f1 | xargs)
local win_app=$(echo "$win" | cut -d'|' -f2 | xargs)
local win_ws=$(echo "$win" | cut -d'|' -f5 | xargs)
# Skip if the monitor is already the target monitor or if the workspace matches
[[ $target_mon != "$win_mon" ]] && return 0
[[ $ws == "$win_ws" ]] && return 0
aerospace move-node-to-workspace --window-id "$win_id" "$ws"
}
# Process each PIP window found
echo "$pip_wins" | while IFS= read -r win; do
move_win "$win"
done
Here's the edited version of the script that I use, for the use case of multiple window titles, if that's of any help
#!/usr/bin/env sh
ws=${1:-$AEROSPACE_FOCUSED_WORKSPACE}
IFS=$'\n' all_wins=$(aerospace list-windows --all --format '%{window-id}|%{app-name}|%{window-title}|%{monitor-id}|%{workspace}')
IFS=$'\n' all_ws=$(aerospace list-workspaces --all --format '%{workspace}|%{monitor-id}')
target_mon=$(printf '%s\n' $all_ws | rg "^$ws" | cut -d'|' -f2 | xargs)
move_win() {
local filter="$1"
local win="$(printf '%s\n' $all_wins | rg "$filter")"
[[ -n $win ]] || return 0
local win_mon=$(printf $win | cut -d'|' -f4 | xargs)
local win_id=$(printf $win | cut -d'|' -f1 | xargs)
local win_app=$(printf $win | cut -d'|' -f2 | xargs)
local win_ws=$(printf $win | cut -d'|' -f5 | xargs)
[[ $target_mon != $win_mon ]] && return 0
[[ $ws == $win_ws ]] && return 0
aerospace move-node-to-workspace --window-id $win_id $ws
}
# YouTube Picture in Picture
move_win 'Picture in Picture'
# Google Meet Window
move_win 'about:blank - [Your Chrome Profile]'
I have the following script (defined as
on-ws-change.shand executable) to move Chrome PiP window:#!/usr/bin/env sh ws=${1:-$AEROSPACE_FOCUSED_WORKSPACE} IFS=$'\n' all_wins=$(aerospace list-windows --all --format '%{window-id}|%{app-name}|%{window-title}|%{monitor-id}|%{workspace}') IFS=$'\n' all_ws=$(aerospace list-workspaces --all --format '%{workspace}|%{monitor-id}') chrome_pip=$(printf '%s\n' $all_wins | rg 'Picture in Picture') target_mon=$(printf '%s\n' $all_ws | rg "$ws" | cut -d'|' -f2 | xargs) move_win() { local win="$1" [[ -n $win ]] || return 0 local win_mon=$(printf $win | cut -d'|' -f4 | xargs) local win_id=$(printf $win | cut -d'|' -f1 | xargs) local win_app=$(printf $win | cut -d'|' -f2 | xargs) local win_ws=$(printf $win | cut -d'|' -f5 | xargs) [[ $target_mon != $win_mon ]] && return 0 [[ $ws == $win_ws ]] && return 0 aerospace move-node-to-workspace --window-id $win_id $ws } move_win "${chrome_pip}"And in my
aerospace.toml, I have this:exec-on-workspace-change = ['<full-path-to>/on-ws-change.sh']
This works well for me. Thank you. 👍
I've updated @farzadmf 's script with support for different pip window titles. I tried to get it to support multple pip windows at once but no luck yet.
#!/usr/bin/env sh # This seems to only work a single pip window at a time for now ws=${1:-$AEROSPACE_FOCUSED_WORKSPACE} IFS=$'\n' all_wins=$(aerospace list-windows --all --format '%{window-id}|%{app-name}|%{window-title}|%{monitor-id}|%{workspace}') IFS=$'\n' all_ws=$(aerospace list-workspaces --all --format '%{workspace}|%{monitor-id}') # Array of possible window titles pip_titles=("Picture-in-picture" "Picture-in-Picture" "Picture in Picture" "Picture in picture") # Function to find matching PIP windows find_pip_windows() { local titles=("$@") local result="" for title in "${titles[@]}"; do local matches=$(printf '%s\n' "$all_wins" | rg "$title") result="$result"$'\n'"$matches" done echo "$result" | sed '/^\s*$/d' # Remove empty lines } pip_wins=$(find_pip_windows "${pip_titles[@]}") target_mon=$(printf '%s\n' "$all_ws" | rg "^$ws" | cut -d'|' -f2 | xargs) move_win() { local win="$1" [[ -n $win ]] || return 0 local win_mon=$(echo "$win" | cut -d'|' -f4 | xargs) local win_id=$(echo "$win" | cut -d'|' -f1 | xargs) local win_app=$(echo "$win" | cut -d'|' -f2 | xargs) local win_ws=$(echo "$win" | cut -d'|' -f5 | xargs) # Skip if the monitor is already the target monitor or if the workspace matches [[ $target_mon != "$win_mon" ]] && return 0 [[ $ws == "$win_ws" ]] && return 0 aerospace move-node-to-workspace --window-id "$win_id" "$ws" } # Process each PIP window found echo "$pip_wins" | while IFS= read -r win; do move_win "$win" done
I took this to ChatGPT, and apparently the issue is the echo | while loop construct, which pipes each iteration to a subshell, so the script's state isn't preserved across iterations. This can be easily fixed by replacing with a for loop:
for win in $pip_wins; do
move_win "$win"
done
Now all PIP windows matching the titles in pip_titles will be moved on workspace focus change.
I've updated @farzadmf 's script with support for different pip window titles. I tried to get it to support multple pip windows at once but no luck yet.
#!/usr/bin/env sh # This seems to only work a single pip window at a time for now ws=${1:-$AEROSPACE_FOCUSED_WORKSPACE} IFS=$'\n' all_wins=$(aerospace list-windows --all --format '%{window-id}|%{app-name}|%{window-title}|%{monitor-id}|%{workspace}') IFS=$'\n' all_ws=$(aerospace list-workspaces --all --format '%{workspace}|%{monitor-id}') # Array of possible window titles pip_titles=("Picture-in-picture" "Picture-in-Picture" "Picture in Picture" "Picture in picture") # Function to find matching PIP windows find_pip_windows() { local titles=("$@") local result="" for title in "${titles[@]}"; do local matches=$(printf '%s\n' "$all_wins" | rg "$title") result="$result"$'\n'"$matches" done echo "$result" | sed '/^\s*$/d' # Remove empty lines } pip_wins=$(find_pip_windows "${pip_titles[@]}") target_mon=$(printf '%s\n' "$all_ws" | rg "^$ws" | cut -d'|' -f2 | xargs) move_win() { local win="$1" [[ -n $win ]] || return 0 local win_mon=$(echo "$win" | cut -d'|' -f4 | xargs) local win_id=$(echo "$win" | cut -d'|' -f1 | xargs) local win_app=$(echo "$win" | cut -d'|' -f2 | xargs) local win_ws=$(echo "$win" | cut -d'|' -f5 | xargs) # Skip if the monitor is already the target monitor or if the workspace matches [[ $target_mon != "$win_mon" ]] && return 0 [[ $ws == "$win_ws" ]] && return 0 aerospace move-node-to-workspace --window-id "$win_id" "$ws" } # Process each PIP window found echo "$pip_wins" | while IFS= read -r win; do move_win "$win" doneI took this to ChatGPT, and apparently the issue is the
echo | whileloop construct, which pipes each iteration to a subshell, so the script's state isn't preserved across iterations. This can be easily fixed by replacing with aforloop:for win in $pip_wins; do move_win "$win" doneNow all PIP windows matching the titles in
pip_titleswill be moved on workspace focus change.
Nice it works! Thank you so much.
Can't make this work with Sequoia. PIP stays in the same space
Can't make this work with Sequoia. PIP stays in the same space
works on mac processor...and not on intel for some reason
Any update on this? I feel like this should be labeled a bug as it prevents PIP from behaving like PIP. Hyprland and i3 do this out of box.