├── .gitignore ├── misc └── choice_matching.jpg ├── LICENSE ├── README.md └── layout_manager.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | 4 | html/ 5 | -------------------------------------------------------------------------------- /misc/choice_matching.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klaxalk/i3-layout-manager/HEAD/misc/choice_matching.jpg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Tomáš Báča 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # i3-layout-manager 2 | Saving, loading and managing layouts for i3wm. 3 | 4 | Video: 5 | 6 | [![Play video](https://img.youtube.com/vi/Q0qlUfG_lZ0/0.jpg)](https://www.youtube.com/watch?v=Q0qlUfG_lZ0) 7 | 8 | Features: 9 | * saving a layout (including floating windows) 10 | * restoring the layout on any workspace 11 | * rearranging existing windows as well as preparing the layout for new windows 12 | * layout management using _rofi_ 13 | 14 | ## Preamble - don't worry, I solved all of this 15 | 16 | i3 window manager supports saving and loading of window layouts, however, the features are bare-bone and partially missing. 17 | According to the [manual](https://i3wm.org/docs/layout-saving.html), the layout tree can be exported into a JSON file. 18 | The file contains a description of the containers of a workspace with prefilled (and commented) potential matching rules for the windows. 19 | The user is supposed to uncomment the desired one (and modify them) and delete the unused ones. 20 | Moreover, the user should add a surrounding root container which is missing in the file (this baffles me, why can't they save it too?). 21 | 22 | So doing it manually (which I don't want) consists of following steps, as described at [i3wm.org](https://i3wm.org/docs/layout-saving.html): 23 | 1. export the workspace into JSON using ```i3-save-tree --workspace ...``` 24 | 2. edit the JSON to match your desired matching rules for the windows 25 | 3. wrap the file in a root node, which defines the root split. 26 | 4. when needed, load the layout using ```i3-append ...``` 27 | 28 | However, this plan has flaws. 29 | It's not scalable, it's not automated and loading a layout does not work when windows are already present in the current workspace. 30 | To fix it, I built this **i3-layout-manager**. 31 | Currently, its a hacky-type of a shell script, but feel free to contribute :-). 32 | 33 | ## How does it work? 34 | 35 | 1. The workspace tree is exported using ```i3-save-tree --workspace ...``` 36 | 2. The tree for all workspaces on the currently focused monitor is exported using ```i3-save-tree --output ...``` 37 | 3. The location of the current workspace in the all-tree is found by matching the workspace-tree file on the monitor-tree file. 38 | 4. The parameters of the root split are extracted, and the workspace tree is wrapped in a new split. 39 | 5. The floating windows are extracted from within and appended behind the root split. 40 | 6. The user is then asked about how should the windows be matched. The options are: 41 | * All by _instance_ (instance will be uncommented for all windows) 42 | * Match any window to any placeholder 43 | * Choose an option for each window. The user will be asked to choose between the _class_, _instance_ and _title_ for each window. The tree file will be modified automatically according to the selected options. 44 | ![matching](misc/choice_matching.jpg) 45 | 7. After that, the tree is saved and ready to be loaded. 46 | 8. The user can load the layout either before opening windows, which creates placeholders, or after, which adds the existing windows to the layout. 47 | 9. To apply a layout, we first move all windows containing a process from the workspace using `xdotool`, which leaves only placeholders. Then we remove all the old placeholders before we apply the layout, which spawns new placeholders in the correct places. Lastly, we move the windows back, which triggers the _swallow_ mechanism in the same way, as newly created windows do. 48 | 49 | ## How to use it? 50 | 51 | * By directly running the script 52 | ```bash 53 | ./layout_manager.sh 54 | ``` 55 | It uses *rofi* to interact with the user, no file editing or coding is required. 56 | You can bind the script to an i3 key key combo. 57 | * The layout manager can load a layout by running 58 | ```bssh 59 | ./layout_manager.sh 60 | ``` 61 | which is useful for automation. If the `layout_name` ends with .json, the manager treats the argument as a path to a particular layout file. 62 | 63 | ## Layout files 64 | 65 | The layout files are stored by default in `~/.layouts` or in `~/.config/i3-layout-manager/layouts`, depending on your `$XDG_CONFIG_HOME`. 66 | 67 | ## Dependencies 68 | 69 | * vim/nvim 70 | * jq 71 | * i3 72 | * rofi 73 | * xdotool 74 | * x11-xserver-utils 75 | * indent, libanyevent-i3-perl 76 | 77 | ```bash 78 | sudo apt install jq vim rofi xdotool x11-xserver-utils indent libanyevent-i3-perl 79 | ``` 80 | 81 | ## FAQ 82 | 83 | * **Does it work on floating windows?** 84 | 85 | Yes, sometimes. Some programs behave strangely, e.g., the *Thunar file* manager fails to load into a floating place holder. 86 | 87 | * **Will it run the programs for me?** 88 | 89 | Nope. It is not intended to do that. The layout manager only automates the already built-in features of i3. Running programs is a different matter than applying layout. 90 | 91 | * **Does it move windows across workspaces?** 92 | 93 | No, it only affects the current workspace. However, layouts can be used on another workspace than they had been created on. 94 | 95 | * **Why do you use vim for the automated file editing?** 96 | 97 | Vim is great for this kind of work. A simple one-liner can do complex edits which would be difficult to program even using, e.g., python. Thanks to this, the layout manager was hacked up in a single day. 98 | 99 | ## Troubleshooting 100 | * On Arch Linux, there is no package `libanyevent-i3-perl`, so my saved layout file says `Can't locate AnyEvent/I3.pm in @INC (you may need to install the AnyEvent::I3 module)` 101 | * Install `perl-anyevent-i3` with your package manager. ([Source](https://old.reddit.com/r/archlinux/comments/289g9u/i3_48_introduces_layout_saving_and_restoring/ci8saf0/)) 102 | * Your system locale must be set to something that uses UTF8, otherwise you will see ```xkbcommon``` errors. 103 | * https://wiki.archlinux.org/title/locale#Setting_the_system_locale 104 | * Arch linux removed the x11-xserver-utils package. You can install the ```xorg-apps``` package instead. 105 | * https://www.reddit.com/r/archlinux/comments/69r15f/xorgserverutils_removed/ 106 | -------------------------------------------------------------------------------- /layout_manager.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Author: klaxalk (klaxalk@gmail.com, github.com/klaxalk) 3 | # 4 | # Dependencies: 5 | # - vim/nvim : scriptable file editing 6 | # - jq : json manipulation 7 | # - rofi : nice dmenu alternative 8 | # - xdotool : window manipulation 9 | # - xrandr : getting info of current monitor 10 | # - i3-msg : i3 tui 11 | # - awk+sed+cat ... 12 | # 13 | # vim: set foldmarker=#\ #{,#\ #} 14 | 15 | # #{ CHECK DEPENDENCIES 16 | 17 | # Detect Linux distribution 18 | DISTRO=$(lsb_release -is 2>/dev/null || echo "Unkown") 19 | 20 | # Function to install a package based on the detected DISTRO 21 | install_package(){ 22 | PACKAGE=$1 23 | case "$DISTRO" in 24 | Ubuntu|Debian) 25 | sudo apt update && sudo apt install -y "$PACKAGE" 26 | ;; 27 | Fedora) 28 | sudo dnf install -y "$PACKAGE" 29 | ;; 30 | openSUSE|SUSE) 31 | sudo zypper install -y "$PACKAGE" 32 | ;; 33 | Arch) 34 | sudo pacman -Syu "$PACKAGE" --noconfirm 35 | ;; 36 | *) 37 | echo "Unsupported distribution: $DISTRO" 38 | exit 1 39 | ;; 40 | esac 41 | } 42 | 43 | # Check dependencies and collect missing ones 44 | MISSING_DEPENDENCIES=() 45 | check_dependency() { 46 | BIN_PATH=$(whereis -b "$1" | awk '{print $2}') 47 | if [ -z "$BIN_PATH" ]; then 48 | MISSING_DEPENDENCIES+=("$2") 49 | fi 50 | } 51 | 52 | # List of dependencies and their package names 53 | check_dependency "vim" "vim" 54 | check_dependency "nvim" "neovim" 55 | check_dependency "jq" "jq" 56 | check_dependency "xdotool" "xdotool" 57 | check_dependency "xrandr" "xrandr" 58 | check_dependency "rofi" "rofi" 59 | 60 | # Ensure at least one editor is available 61 | if [ -z "$(whereis -b vim | awk '{print $2}')" ] && [ -z "$(whereis -b nvim | awk '{print $2}')" ]; then 62 | MISSING_DEPENDENCIES+=("vim") 63 | fi 64 | 65 | # Prompt to install missing dependencies 66 | if [ ${#MISSING_DEPENDENCIES[@]} -gt 0 ]; then 67 | echo "The following dependencies are missing: ${MISSING_DEPENDENCIES[*]}" 68 | read -p "Do you want to install them? [y/N]: " RESPONSE 69 | if [[ "$RESPONSE" =~ ^[Yy]$ ]]; then 70 | for PACKAGE in "${MISSING_DEPENDENCIES[@]}"; do 71 | install_package "$PACKAGE" 72 | done 73 | else 74 | echo "Skipping installation of dependencies. The script may not work as expected." 75 | exit 1 76 | fi 77 | fi 78 | # #} 79 | 80 | if [ -z "$XDG_CONFIG_HOME" ]; then 81 | if [ -e "${HOME}/.layouts" ] && [ -n "$(command ls -A ${HOME}/.layouts)" ]; then 82 | LAYOUT_PATH=${HOME}/.layouts 83 | else 84 | LAYOUT_PATH="${HOME}/.config/i3-layout-manager/layouts" 85 | fi 86 | else 87 | LAYOUT_PATH="$XDG_CONFIG_HOME/i3-layout-manager/layouts" 88 | fi 89 | 90 | # make directory for storing layouts 91 | mkdir -p $LAYOUT_PATH > /dev/null 2>&1 92 | 93 | # logs 94 | LOG_FILE=/tmp/i3_layout_manager.txt 95 | echo "" > "$LOG_FILE" 96 | 97 | # #{ ASK FOR THE ACTION 98 | 99 | # if operating using dmenu 100 | if [ -z $1 ]; then 101 | 102 | ACTION=$(echo "LOAD LAYOUT 103 | SAVE LAYOUT 104 | DELETE LAYOUT" | rofi -i -dmenu -no-custom -p "Select action") 105 | 106 | if [ -z "$ACTION" ]; then 107 | exit 108 | fi 109 | 110 | # get me layout names based on existing file names in the LAYOUT_PATH 111 | LAYOUT_NAMES=$(ls -Rt $LAYOUT_PATH | grep "layout.*json" | sed -nr 's/layout-(.*)\.json/\1/p' | sed 's/\s/\n/g' | sed 's/_/ /g') # layout names 112 | LAYOUT_NAME=$(echo "$LAYOUT_NAMES" | rofi -i -dmenu -p "Select layout (you may type new name when creating)" | sed 's/\s/_/g') # ask for selection 113 | LAYOUT_NAME=${LAYOUT_NAME^^} # upper case 114 | 115 | # getting argument from command line 116 | else 117 | 118 | ACTION="LOAD LAYOUT" 119 | # if the layout name is a full path, just pass it, otherwise convert it to upper case 120 | if [[ "${1}" == *".json" ]]; then 121 | LAYOUT_NAME="${1}" 122 | else 123 | LAYOUT_NAME="${1^^}" 124 | fi 125 | 126 | fi 127 | 128 | # no action, exit 129 | if [ -z "$LAYOUT_NAME" ]; then 130 | exec "$0" "$@" 131 | fi 132 | 133 | # #} 134 | 135 | # if the layout name is a full path, use it, otherwise fabricate the full path 136 | if [[ $LAYOUT_NAME == *".json" ]]; then 137 | LAYOUT_FILE=`realpath "$LAYOUT_NAME"` 138 | else 139 | LAYOUT_FILE=$LAYOUT_PATH/layout-"$LAYOUT_NAME".json 140 | fi 141 | 142 | echo $LAYOUT_FILE 143 | 144 | if [ "$ACTION" == "LOAD LAYOUT" ] && [ ! -f "$LAYOUT_FILE" ]; then 145 | exit 146 | fi 147 | 148 | # get current workspace ID 149 | WORKSPACE_ID=$(i3-msg -t get_workspaces | jq '.[] | select(.focused==true).num' | cut -d"\"" -f2) 150 | 151 | # #{ LOAD 152 | 153 | if [[ "$ACTION" = "LOAD LAYOUT" ]]; then 154 | 155 | # updating the workspace to the new layout is tricky 156 | # normally it does not influence existing windows 157 | # For it to apply to existing windows, we need to 158 | # first remove them from the workspace and then 159 | # add them back while we remove any empty placeholders 160 | # which would normally cause mess. The placeholders 161 | # are recognize by having no process inside them. 162 | 163 | # get the list of windows on the current workspace 164 | WINDOWS=$(xdotool search --all --onlyvisible --desktop $(xprop -notype -root _NET_CURRENT_DESKTOP | cut -c 24-) "" 2>/dev/null) 165 | 166 | echo "About to unload all windows from the workspace" >> "$LOG_FILE" 167 | 168 | for window in $WINDOWS; do 169 | 170 | # the grep filters out a line which reports on the command that was just being called 171 | # however, the line is not there when calling with rofi from i3 172 | HAS_PID=$(xdotool getwindowpid $window 2>&1 | grep -v command | wc -l) 173 | 174 | echo "Unloading window '$window'" >> "$LOG_FILE" 175 | 176 | if [ $HAS_PID -eq 0 ]; then 177 | echo "Window '$window' does not have a process" >> "$LOG_FILE" 178 | else 179 | xdotool windowunmap "$window" >> "$LOG_FILE" 2>&1 180 | echo "'xdotool windounmap $window' returned $?" >> "$LOG_FILE" 181 | fi 182 | 183 | done 184 | 185 | echo "" >> "$LOG_FILE" 186 | echo "About to delete all empty window placeholders" >> "$LOG_FILE" 187 | 188 | # delete all empty layout windows from the workspace 189 | # we just try to focus any window on the workspace (there should not be any, we unloaded them) 190 | for (( i=0 ; $a-100 ; a=$a+1 )); do 191 | 192 | # check window for STICKY before killing - if sticky do not kill 193 | xprop -id $(xdotool getwindowfocus) | grep -q '_NET_WM_STATE_STICK' 194 | 195 | if [ $? -eq 1 ]; then 196 | 197 | echo "Killing an unsued placeholder" >> "$LOG_FILE" 198 | i3-msg "focus parent, kill" >> "$LOG_FILE" 2>&1 199 | 200 | i3_msg_ret="$?" 201 | 202 | if [ "$i3_msg_ret" == 0 ]; then 203 | echo "Empty placeholder successfully killed" >> "$LOG_FILE" 204 | else 205 | echo "Empty placeholder could not be killed, breaking" >> "$LOG_FILE" 206 | break 207 | fi 208 | fi 209 | done 210 | 211 | echo "" >> "$LOG_FILE" 212 | echo "Applying the layout" >> "$LOG_FILE" 213 | 214 | # then we can apply to chosen layout 215 | i3-msg "append_layout $LAYOUT_FILE" >> "$LOG_FILE" 2>&1 216 | 217 | echo "" >> "$LOG_FILE" 218 | echo "About to bring all windows back" >> "$LOG_FILE" 219 | 220 | # and then we can reintroduce the windows back to the workspace 221 | for window in $WINDOWS; do 222 | 223 | # the grep filters out a line which reports on the command that was just being called 224 | # however, the line is not there when calling with rofi from i3 225 | HAS_PID=$(xdotool getwindowpid $window 2>&1 | grep -v command | wc -l) 226 | 227 | echo "Loading back window '$window'" >> "$LOG_FILE" 228 | 229 | if [ $HAS_PID -eq 0 ]; then 230 | echo "$window does not have a process" >> "$LOG_FILE" 231 | else 232 | xdotool windowmap "$window" 233 | echo "'xdotool windowmap $window' returned $?" >> "$LOG_FILE" 234 | fi 235 | done 236 | 237 | fi 238 | 239 | # #} 240 | 241 | # #{ SAVE 242 | 243 | if [[ "$ACTION" = "SAVE LAYOUT" ]]; then 244 | 245 | ACTION=$(echo "DEFAULT (INSTANCE) 246 | SPECIFIC (CHOOSE) 247 | MATCH ANY" | rofi -i -dmenu -p "How to identify windows? (xprop style)") 248 | 249 | 250 | if [[ "$ACTION" = "DEFAULT (INSTANCE)" ]]; then 251 | CRITERION="default" 252 | elif [[ "$ACTION" = "SPECIFIC (CHOOSE)" ]]; then 253 | CRITERION="specific" 254 | elif [[ "$ACTION" = "MATCH ANY" ]]; then 255 | CRITERION="any" 256 | fi 257 | 258 | ALL_WS_FILE=$LAYOUT_PATH/all-layouts.json 259 | 260 | CURRENT_MONITOR=$(i3-msg -t get_workspaces | jq '.[] | select(.focused==true).output' | cut -d"\"" -f2) 261 | 262 | # get the i3-tree for all workspaces for the current monitor 263 | i3-save-tree --output "$CURRENT_MONITOR" > "$ALL_WS_FILE" 2>&1 264 | 265 | # get the i3-tree for the current workspace 266 | i3-save-tree --workspace "$WORKSPACE_ID" > "$LAYOUT_FILE" 2>&1 267 | 268 | # for debug 269 | # cp $LAYOUT_FILE $LAYOUT_PATH/ws_temp.txt 270 | # cp $ALL_WS_FILE $LAYOUT_PATH/all_temp.txt 271 | 272 | # back the output file.. we are gonna modify it and alter we will need it back 273 | BACKUP_FILE=$LAYOUT_PATH/.layout_backup.txt 274 | cp $LAYOUT_FILE $BACKUP_FILE 275 | 276 | # get me vim, we will be using it alot to postprocess the generated json files 277 | if [ -x "$(whereis nvim | awk '{print $2}')" ]; then 278 | VIM_BIN="$(whereis nvim | awk '{print $2}')" 279 | HEADLESS="--headless" 280 | GOT_NVIM=true 281 | elif [ -x "$(whereis vim | awk '{print $2}')" ]; then 282 | VIM_BIN="$(whereis vim | awk '{print $2}')" 283 | HEADLESS="" 284 | GOT_VIM=true 285 | fi 286 | 287 | # the allaround task is to produce a single json file with the description 288 | # of the current layout on the focused workspace. However, the 289 | # i3-save-tree --workspace 290 | # command only outputs the inner containers, without wrapping them into the 291 | # root container of the workspace, which leads to loosing the information 292 | # about the initial split .. vertical? or horizontal?... 293 | # We can solve it by asking for a tree, which contains all workspaces, 294 | # including the root splits and borrowing the root split info from there. 295 | # I do it by locating the right place in the all-tree by mathing the 296 | # workspace tree and then extracting the split part and adding it back 297 | # to the workspace json. 298 | 299 | # first we need to do some preprocessing, before we can find, where in the 300 | # all-tree file we can find the workspace part. 301 | 302 | # remove the floating window part, that would screw up out matching 303 | if [ -n "$GOT_VIM" ]; then 304 | $VIM_BIN $HEADLESS -nEs -u NONE -c '%g/floating_con/norm [{d%' -c "wqa" -- "$LAYOUT_FILE" 305 | else 306 | # when scripting d% to delete to the next in pair, it actually leaves one of the pair characters there 307 | $VIM_BIN $HEADLESS -nEs -u NONE -c '%g/floating_con/norm [{d%dd' -c "wqa" -- "$LAYOUT_FILE" 308 | fi 309 | 310 | # remove comments 311 | $VIM_BIN $HEADLESS -nEs -u NONE -c '%g/\/\//norm dd' -c "wqa" -- "$LAYOUT_FILE" 312 | $VIM_BIN $HEADLESS -nEs -u NONE -c '%g/\/\//norm dd' -c "wqa" -- "$ALL_WS_FILE" 313 | 314 | # remove indents 315 | $VIM_BIN $HEADLESS -nEs -u NONE -c '%g/^/norm 0d^' -c "wqa" -- "$LAYOUT_FILE" 316 | $VIM_BIN $HEADLESS -nEs -u NONE -c '%g/^/norm 0d^' -c "wqa" -- "$ALL_WS_FILE" 317 | 318 | # remove commas 319 | $VIM_BIN $HEADLESS -nEs -u NONE -c '%s/^},$/}/g' -c "wqa" -- "$LAYOUT_FILE" 320 | $VIM_BIN $HEADLESS -nEs -u NONE -c '%s/^},$/}/g' -c "wqa" -- "$ALL_WS_FILE" 321 | 322 | # remove empty lines in the the workspace file 323 | $VIM_BIN $HEADLESS -nEs -u NONE -c '%g/^$/norm dd' -c "wqa" -- "$LAYOUT_FILE" 324 | 325 | # now I will try to find the part in the big file which containts the 326 | # small file. I have not found a suitable solution using off-the-shelf 327 | # tools, so custom bash it is... 328 | 329 | MATCH=0 330 | PATTERN_LINES=`cat $LAYOUT_FILE | wc -l` # get me the number of lines in the small file 331 | SOURCE_LINES=`cat $ALL_WS_FILE | wc -l` # get me the number of lines in the big file 332 | 333 | N_ITER=$(expr $SOURCE_LINES - $PATTERN_LINES) 334 | readarray pattern < $LAYOUT_FILE 335 | 336 | MATCH_LINE=0 337 | for (( a=1 ; $a-$N_ITER ; a=$a+1 )); do 338 | 339 | CURR_LINE=0 340 | MATCHED_LINES=0 341 | while read -r line1; do 342 | 343 | PATTERN_LINE=$(echo ${pattern[$CURR_LINE]} | tr -d '\n') 344 | 345 | if [[ "$line1" == "$PATTERN_LINE" ]]; then 346 | MATCHED_LINES=$(expr $MATCHED_LINES + 1) 347 | else 348 | break 349 | fi 350 | 351 | CURR_LINE=$(expr $CURR_LINE + 1) 352 | done <<< $(cat "$ALL_WS_FILE" | tail -n +"$a") 353 | 354 | if [[ "$MATCHED_LINES" == "$PATTERN_LINES" ]]; 355 | then 356 | MATCH_LINE="$a" 357 | break 358 | fi 359 | done 360 | 361 | # lets extract the key part, containing the block with the root split 362 | 363 | # load old workspace file (we destroyed the old one, remember?) 364 | mv $BACKUP_FILE $LAYOUT_FILE 365 | 366 | $VIM_BIN $HEADLESS -nEs -u NONE -c '%s/\\\\//g' -c "wqa" -- "$LAYOUT_FILE" 367 | 368 | # delete the part below and above the block 369 | $VIM_BIN $HEADLESS -nEs -u NONE -c "normal ${MATCH_LINE}ggdGG{kdgg" -c "wqa" -- "$ALL_WS_FILE" 370 | # rename the "workspace to "con" (container) 371 | $VIM_BIN $HEADLESS -nEs -u NONE -c '%g/type/norm ^Wlciwcon' -c "wqa" -- "$ALL_WS_FILE" 372 | # change the fullscrean to 0 373 | $VIM_BIN $HEADLESS -nEs -u NONE -c '%g/fullscreen/norm ^Wr0' -c "wqa" -- "$ALL_WS_FILE" 374 | 375 | # extract the needed part of the file and add it to the workspace file 376 | # this part is mostly according to the i3 manual, except we actually put there 377 | # the information about the split type 378 | cat $ALL_WS_FILE | cat - $LAYOUT_FILE > /tmp/tmp.txt && mv /tmp/tmp.txt $LAYOUT_FILE 379 | # add closing bracked at the end 380 | $VIM_BIN $HEADLESS -nEs -u NONE -c 'normal Go] }' -c "wqa" -- "$LAYOUT_FILE" 381 | 382 | # now we have to do some postprocessing on it, all is even advices on the official website 383 | # https://i3wm.org/docs/layout-saving.html 384 | 385 | # uncomment the instance swallow rule 386 | if [[ "$CRITERION" = "default" ]]; then 387 | $VIM_BIN $HEADLESS -nEs -u NONE -c "%g/instance/norm ^dW" -c "wqa" -- "$LAYOUT_FILE" 388 | elif [[ "$CRITERION" = "any" ]]; then 389 | $VIM_BIN $HEADLESS -nEs -u NONE -c '%g/instance/norm ^dW3f"di"i^.+$' -c "wqa" -- "$LAYOUT_FILE" 390 | elif [[ "$CRITERION" = "specific" ]]; then 391 | 392 | LAST_LINE=1 393 | 394 | while true; do 395 | 396 | LINE_NUM=$(cat $LAYOUT_FILE | tail -n +$LAST_LINE | grep '// "class' -n | awk '{print $1}') 397 | HAS_INSTANCE=$(echo $LINE_NUM | wc -l) 398 | 399 | if [ ! -z "$LINE_NUM" ]; then 400 | 401 | LINE_NUM=$(echo $LINE_NUM | awk '{print $1}') 402 | LINE_NUM=${LINE_NUM%:} 403 | LINE_NUM=$(expr $LINE_NUM - 1) 404 | LINE_NUM=$(expr $LINE_NUM + $LAST_LINE ) 405 | 406 | NAME=$(cat $LAYOUT_FILE | sed -n "$(expr ${LINE_NUM} - 4)p" | awk '{$1="";print $0}') 407 | 408 | SELECTED_OPTION=$(cat -n $LAYOUT_FILE | sed -n "${LINE_NUM},$(expr $LINE_NUM + 2)p" | awk '{$2="";print $0}' | rofi -i -dmenu -no-custom -p "Choose the matching method for${NAME%,}" | awk '{print $1}') 409 | 410 | # when user does not select, choose "instance" (class+1) 411 | if [ -z "$SELECTED_OPTION" ]; then 412 | SELECTED_OPTION=$(expr ${LINE_NUM} + 1) 413 | fi 414 | 415 | $VIM_BIN $HEADLESS -nEs -u NONE -c "norm ${SELECTED_OPTION}gg^dW" -c "wqa" -- "$LAYOUT_FILE" 416 | 417 | LAST_LINE=$( expr $SELECTED_OPTION) 418 | 419 | else 420 | break 421 | fi 422 | 423 | done 424 | fi 425 | 426 | # uncomment the transient_for 427 | $VIM_BIN $HEADLESS -nEs -u NONE -c '%g/transient_for/norm ^dW' -c "wqa" -- "$LAYOUT_FILE" 428 | 429 | # delete all comments 430 | $VIM_BIN $HEADLESS -nEs -u NONE -c '%g/\/\//norm dd' -c "wqa" -- "$LAYOUT_FILE" 431 | 432 | # add a missing comma to the last element of array we just deleted 433 | $VIM_BIN $HEADLESS -nEs -u NONE -c '%g/swallows/norm j^%k:s/,$//g ' -c "wqa" -- "$LAYOUT_FILE" 434 | 435 | # delete all empty lines 436 | $VIM_BIN $HEADLESS -nEs -u NONE -c '%g/^$/norm dd' -c "wqa" -- "$LAYOUT_FILE" 437 | 438 | # pick up floating containers and move them out of the root container 439 | if [ -n "$GOT_VIM" ]; then 440 | $VIM_BIN $HEADLESS -nEs -u NONE -c '%g/floating_con/norm [{d%GA p' -c "wqa" -- "$LAYOUT_FILE" 441 | else 442 | # nvim has a bug currently: 443 | # when scripting d% to delete to the next in pair, it actually leaves one of the pair characters there 444 | $VIM_BIN $HEADLESS -nEs -u NONE -c "%g/floating_con/norm [{%ma%d'aGA p" -c "wqa" -- "$LAYOUT_FILE" 445 | fi 446 | 447 | # delete all empty lines 448 | $VIM_BIN $HEADLESS -nEs -u NONE -c '%g/^$/norm dd' -c "wqa" -- "$LAYOUT_FILE" 449 | 450 | # add missing commas between the newly created inner parts of the root element 451 | $VIM_BIN $HEADLESS -nEs -u NONE -c '%s/}\n{/}, {/g' -c "wqa" -- "$LAYOUT_FILE" 452 | 453 | # surroun everythin in [] 454 | $VIM_BIN $HEADLESS -nEs -u NONE -c 'normal ggO[Go]' -c "wqa" -- "$LAYOUT_FILE" 455 | 456 | # autoformat the file 457 | $VIM_BIN $HEADLESS -nEs -u NONE -c 'normal gg=G' -c "wqa" -- "$LAYOUT_FILE" 458 | 459 | rm "$ALL_WS_FILE" 460 | 461 | notify-send -u low -t 2000 "Layout saved" -h string:x-canonical-private-synchronous:anything 462 | 463 | fi 464 | 465 | # #} 466 | 467 | # #{ DELETE 468 | 469 | if [[ "$ACTION" = "DELETE LAYOUT" ]]; then 470 | rm "$LAYOUT_FILE" 471 | notify-send -u low -t 2000 "Layout deleted" -h string:x-canonical-private-synchronous:anything 472 | exec "$0" "$@" 473 | fi 474 | 475 | # #} 476 | --------------------------------------------------------------------------------