├── .gitignore ├── LICENSE ├── README.md ├── dmenu-ag.sh ├── i3_master ├── i3_master.ini ├── i3_master_layout.py ├── i3_swallow.py ├── rofi-ag.sh ├── screenshot ├── first_terminal.png ├── swallow_vifm.gif └── swap_master.gif ├── swallow └── temp.txt /.gitignore: -------------------------------------------------------------------------------- 1 | id.txt 2 | __pycache__/**/* 3 | .vim/**/* 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Philipp Schaffrath 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 | # FEATURE 2 | 3 | master and stack layout in i3 4 | ``` 5 | | ------ | ----- | 6 | | | | 7 | | Master | Stack | 8 | | | | 9 | | ------ | ----- | 10 | ``` 11 | 12 | * implement master and stack layout like dwm in i3 13 | 14 | * open first terminal in floating mode and default position 15 | > you don't need to open the first terminal full screen. 16 | > It will better if the terminal is display floating center on your screen 17 | > when you open another window it will change to tilling mode . 18 | 19 | ![first terminal display](./screenshot/first_terminal.png) 20 | * swap from any window to master with shorcut `$mod+m` 21 | * move from any window to master with shorcut `$mod+shift+m` 22 | 23 | ![swap master ](./screenshot/swap_master.gif) 24 | 25 | * swallow instance 26 | > this version is focus on i3 master and it is different to another swallow version 27 | > because it will try to restore the original position when you move the swallow instance 28 | 29 | ![swallow vifm](./screenshot/swallow_vifm.gif) 30 | # Dependencies 31 | 32 | 1. python3 33 | 2. [i3ipc-python](https://github.com/altdesktop/i3ipc-python) 34 | 3. xdotool 35 | 4. xprop 36 | 5. xdo 37 | 38 | 39 | # Install 40 | 41 | Install python 3 and install i3ipc libary 42 | 43 | `pip3 install i3ipc` 44 | 45 | Install xdotool xprop and xdo 46 | 47 | download this script and put it to your i3 config folder and run 48 | 49 | ```bash 50 | cd ~/.config/i3/ 51 | 52 | git clone https://github.com/windwp/i3-master-stack.git 53 | 54 | ``` 55 | ## Run with i3 56 | 57 | put it to your i3 config 58 | 59 | ```bash 60 | exec --no-startup-id $HOME/.config/i3/i3-master-stack/i3_master 61 | # swap to master node 62 | bindsym $mod+m nop swap master 63 | # go to master node 64 | bindsym $mod+shift+m nop go master 65 | # enable/disable master layout in current workspace 66 | bindsym $mod+alt+m nop master toggle 67 | 68 | ``` 69 | reload i3 and testing layout 70 | 71 | ## Run from terminal 72 | ```bash 73 | cd ~/.config/i3/i3-master-stack 74 | python3 ./i3_master_layout.py 75 | ``` 76 | you can run it use bash file [i3_master](./i3_master) 77 | ```bash 78 | ./i3_master 79 | ``` 80 | 81 | # Config 82 | 83 | run script first and it will create a config file 84 | 85 | `$HOME/.config/i3/i3_master.ini` 86 | 87 | ``` ini 88 | [config] 89 | terminal = 'Alacritty' 90 | screenWidth = 1300 91 | screenHeight = 800 92 | posX = 310 93 | posY = 160 94 | swallow = true 95 | 96 | ; different size between master and slave (unit : ppt) 97 | masterSizePlus = 14 98 | 99 | ; new instance on master will change to master 100 | slaveStack = true 101 | ``` 102 | 103 | > Note: Use `xdotool selectwindow getwindowgeometry` change size and get a good postion on floating window 104 | 105 | 106 | # File manager with swallow 107 | 108 | if your file manager is not working with the swallow function. You need to add scripts. 109 | >Example vifm 110 | * Copy file [swallow](./swallow) to folder `$HOME/.config/vifm/scripts/`. 111 | * Edit /vifmrc 112 | ``` 113 | filextype *.bmp,*.jpg,*.jpeg,*.png,*.gif,*.xpm 114 | \ {View in feh} 115 | \ swallow feh %f, 116 | \ {View in gpicview} 117 | \ gpicview %c, 118 | \ {View in shotwell} 119 | \ shotwell, 120 | ``` 121 | # TODO 122 | 123 | - [ ] Swallow when stack have 1 instance is bad 124 | 125 | - [ ] Swallow use xprop and xdotool is slow. 126 | 127 | -------------------------------------------------------------------------------- /dmenu-ag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # use dmenu to excute command because perfomance of rofi 3 | # use need to patch dmenu with center and border with patch 4 | dmenuCommand="dmenu -i -bw 2 -l 10 -fn 'Hack' -p '' -c " 5 | # dmenuCommand="dmenu -i -l 10 -fn 'Hack' -p '✟' " 6 | command="./rofi-ag.sh | ${dmenuCommand}" 7 | status="2" 8 | result=" " 9 | 10 | 11 | DIR=`dirname $0` 12 | TMP_DIR="/tmp/rofi/${USER}/" 13 | TMP_DIR="/tmp/rofi/${USER}/" 14 | HIST_FILE="${TMP_DIR}history.txt" 15 | 16 | 17 | if [ ! -d "${TMP_DIR}" ] 18 | then 19 | mkdir -p "${TMP_DIR}"; 20 | fi 21 | 22 | while [[ "$result" != "exit" && ! -z "$result" ]] 23 | do 24 | agresult=$( eval "$DIR/rofi-ag.sh $result" ) 25 | status=$? 26 | # echo "STATUS: ${status}" 27 | if [[ "${status}" != "1" ]]; then 28 | result=$(eval "cat $HIST_FILE | ${dmenuCommand} ") 29 | if [[ ! -z "${result}" ]]; then 30 | printf -v result "%q\n" "$result" 31 | else 32 | result="exit" 33 | fi 34 | else 35 | result="exit" 36 | fi 37 | done 38 | 39 | # echo "done" 40 | -------------------------------------------------------------------------------- /i3_master: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | echo "start" 3 | IDFILE="/tmp/i3_master_id.txt" 4 | if [ ! -f "${IDFILE}" ] 5 | then 6 | touch "$IDFILE" 7 | fi 8 | echo " Kill id" | cat $IDFILE 9 | kill $(cat "$IDFILE") 10 | DIR=`dirname $0` 11 | # python3 ./i3-swallow.py & echo $! > id.txt 12 | python3 $DIR/i3_master_layout.py --debug & echo $! > $IDFILE 13 | 14 | -------------------------------------------------------------------------------- /i3_master.ini: -------------------------------------------------------------------------------- 1 | [config] 2 | terminal = 'Alacritty' 3 | screenWidth = 1300 4 | screenHeight = 800 5 | posX = 310 6 | posY = 160 7 | swallow = true 8 | 9 | ; different size between master and slave (unit : ppt) 10 | masterSizePlus = 14 11 | 12 | ; new instance on master is change to master 13 | slaveStack = true 14 | 15 | 16 | -------------------------------------------------------------------------------- /i3_master_layout.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # --------------------------------------- 3 | 4 | import argparse 5 | import configparser 6 | import os 7 | import shutil 8 | import subprocess 9 | from pprint import pprint 10 | from time import sleep 11 | 12 | import i3ipc 13 | 14 | import i3_swallow 15 | 16 | rootMark = "root" 17 | masterMark = "master" 18 | slaveMark = "slave" 19 | 20 | 21 | class I3MasterConfig(object): 22 | def __init__(self): 23 | self.terminal = 'Alacritty' 24 | self.screenWidth = 1300 25 | self.screenHeight = 800 26 | self.posX = 310 27 | self.posY = 160 28 | self.firstScreenPercent = 14 # different size between master and slave (unit : ppt) 29 | self.limitWindowOnMaster = 2 30 | self.isEnableSwallow = True 31 | self.isSwapMasterOnNewInstance = True # new instance on master is change to master 32 | pass 33 | 34 | 35 | def dumpNode(node): 36 | result = {} 37 | result["type"] = node["type"] 38 | result["window"] = node["window"] 39 | result["layout"] = node["layout"] 40 | result["percent"] = node["percent"] 41 | result["nodes"] = [] 42 | if(node.get('marks') != None): 43 | result['marks'] = node['marks'] 44 | if node.get("window_properties") != None: 45 | result["title"] = node["window_properties"]["instance"] + \ 46 | " - " + node["window_properties"]["title"] 47 | if len(node["nodes"]) > 0: 48 | result["nodes"] = [] 49 | for node in node["nodes"]: 50 | result["nodes"].append(dumpNode(node)) 51 | if(len(node["floating_nodes"]) > 0): 52 | result["floating_nodes"] = [] 53 | for node in node["floating_nodes"]: 54 | result["floating_nodes"].append(dumpNode(node)) 55 | 56 | return result 57 | 58 | 59 | def dumpWorkSpace(workspace: i3ipc.Con): 60 | result = {} 61 | result["types"] = workspace["type"] 62 | result["workspace_layout"] = workspace["workspace_layout"] 63 | if len(workspace["nodes"]) >= 0: 64 | result["nodes"] = [] 65 | for node in workspace["nodes"]: 66 | result["nodes"].append(dumpNode(node)) 67 | pass 68 | if len(workspace["floating_nodes"]) >= 0: 69 | result["floating_nodes"] = [] 70 | for node in workspace["floating_nodes"]: 71 | result["floating_nodes"].append(dumpNode(node)) 72 | pass 73 | pass 74 | pprint(workspace) 75 | pprint(result) 76 | 77 | 78 | class WorkspaceData(object): 79 | def __init__(self, num: int): 80 | self.num = num 81 | self.swapNodeId = 0 82 | self.masterWidth = 0 83 | self.firstWindowId = 0 84 | self.callback = None 85 | self.isSwallowNext = False 86 | self.isDisable = False 87 | self.slaveMark = slaveMark+"_"+str(num) 88 | self.masterMark = masterMark+"_"+str(num) 89 | self.rootMark = rootMark+"_"+str(num) 90 | pass 91 | 92 | 93 | class I3MasterLayout(object): 94 | 95 | def __init__(self, i3: i3ipc.Con, config: I3MasterConfig, debug=False): 96 | self.i3 = i3 97 | self.masterWidth = 0 98 | self.config=config 99 | self.debug = debug 100 | self.callbacks = {} 101 | self.workSpaceDatas = {} 102 | self.isSwapMasterOnNewInstance = self.config.isSwapMasterOnNewInstance 103 | self.isSwallowNext = False 104 | pass 105 | 106 | def unMarkMasterNode(self, node): 107 | for mark in node.marks: 108 | if mark == masterMark: 109 | self.i3.command('[con_id=%s] unmark' % (node.id)) 110 | return True 111 | for node in node.nodes: 112 | if(self.unMarkMasterNode(node)): 113 | return True 114 | return False 115 | 116 | def getWorkSpaceData(self, workspaceNum) -> WorkspaceData: 117 | ws = self.workSpaceDatas.get(workspaceNum) 118 | if ws == None: 119 | ws = WorkspaceData(workspaceNum) 120 | self.workSpaceDatas[workspaceNum] = ws 121 | return ws 122 | 123 | def getWorkSpaceMark(self, markName, workspaceName): 124 | return markName+"_"+str(workspaceName) 125 | 126 | def findNextNodeToMaser(self, node): 127 | if(node.window != None): 128 | return node 129 | for node in node.nodes: 130 | if(node.window != None): 131 | return node 132 | else: 133 | result = self.findNextNodeToMaser(node) 134 | if result != None: 135 | return result 136 | return None 137 | 138 | def getAllChildWindow(self, root): 139 | result = [] 140 | for node in root.nodes: 141 | if(node.window != None): 142 | result.append(node) 143 | else: 144 | result = result + self.getAllChildWindow(node) 145 | return result 146 | 147 | def findChildNodeByMarked(self, node, mark) -> i3ipc.Con: 148 | for child in node.nodes: 149 | if(mark in child.marks): 150 | return child 151 | else: 152 | result = self.findChildNodeByMarked(child, mark) 153 | if result != None: 154 | return result 155 | return None 156 | 157 | def findChildNodeById(self, node, conId) -> i3ipc.Con: 158 | for child in node: 159 | if child.id == conId : 160 | return child 161 | elif child.nodes != None: 162 | result = self.findChildNodeById(child.nodes, conId) 163 | if result != None: 164 | return result 165 | return None 166 | 167 | def validateMasterAndSlaveNode(self, workspace): 168 | root = workspace 169 | if(root.layout == 'splitv'): 170 | self.i3.command('[con_id=%s] layout splith' % root.id) 171 | 172 | masterNode = None 173 | slaveNode = None 174 | workspaceData = self.getWorkSpaceData(workspace.num) 175 | masterNode = self.findChildNodeByMarked(root, workspaceData.masterMark) 176 | 177 | if(masterNode != None and len(root.nodes) == 1): 178 | # check length root.nodes ==1 because i3 will merge the master node to another node 179 | # then we need to find a better master node from root nodes 180 | root = masterNode.parent 181 | elif (len(root.nodes) > 0): 182 | masterNode = root.nodes[0] 183 | if(len(root.nodes) > 1): 184 | # check if have slave node in current root 185 | for node in root.nodes: 186 | if workspaceData.slaveMark in node.marks: 187 | slaveNode = node 188 | # if don't have set the second node is slave 189 | if(slaveNode == None): 190 | slaveNode = root.nodes[1] 191 | 192 | if(slaveNode == None and masterNode != None): 193 | # try to find the best solutionn for master and slave node 194 | # special case i3 will stack slave node into master node in too many connection 195 | allChild = self.getAllChildWindow(masterNode) 196 | 197 | if(len(allChild) >= 2): 198 | if(masterNode.id != allChild[0].id): 199 | self.i3.command('[con_id=%s] unmark %s' % 200 | (masterNode.id, workspaceData.masterMark)) 201 | root = masterNode.parent 202 | masterNode = allChild[0] 203 | self.i3.command('[con_id=%s] mark %s' % 204 | (masterNode.id, workspaceData.masterMark)) 205 | self.i3.command('[con_id=%s] mark %s' % 206 | (root.id, workspaceData.rootMark)) 207 | if(len(root.nodes) > 1): 208 | slaveNode = root.nodes[1] 209 | else: 210 | # we can't find the best slave node 211 | pass 212 | 213 | # check master node 214 | if(masterNode != None): 215 | if(root.layout == 'splitv'): 216 | # if i3 put the master node to child another node we need to move it to parent node 217 | i3.command('[con_id=%s] move left' % masterNode.id) 218 | 219 | if not workspaceData.masterMark in masterNode.marks: 220 | self.i3.command('[con_id=%s] mark %s' % 221 | (masterNode.id, workspaceData.masterMark)) 222 | if not workspaceData.masterMark in root.marks: 223 | self.i3.command('[con_id=%s] mark %s' % 224 | (root.id, workspaceData.rootMark)) 225 | # check child of masterNode when master is not widow 226 | if(masterNode.window == None): 227 | allChild = self.getAllChildWindow(masterNode) 228 | if( 229 | len(allChild) > self.config.limitWindowOnMaster and 230 | slaveNode != None 231 | ): 232 | # remove all child node on master if have too many 233 | for node in allChild[self.config.limitWindowOnMaster:]: 234 | if(node.window != None): 235 | self.i3.command('[con_id=%s] move window to mark %s' % ( 236 | node.id, workspaceData.slaveMark)) 237 | self.i3.command('[con_id=%s] focus' % (node.id)) 238 | pass 239 | 240 | if(slaveNode != None and masterNode != None): 241 | # mark slave 242 | if not workspaceData.slaveMark in slaveNode.marks: 243 | self.i3.command('[con_id=%s] mark %s' % 244 | (slaveNode.id, workspaceData.slaveMark)) 245 | # if(slaveNode.layout=='splitv'): 246 | # i3.command('[con_id=%s] layout splith' % slaveNode.id) 247 | # cleate layout for slave 248 | if(slaveNode.window != None): 249 | self.i3.command('[con_id=%s] split vertical' % slaveNode.id) 250 | 251 | if(len(root.nodes) > 2): 252 | for node in root.nodes: 253 | # move all child node from root to slave 254 | if node.id != masterNode.id and node.id != slaveNode.id: 255 | self.i3.command('[con_id=%s] move %s to mark %s' 256 | % (node.id, 257 | "container" if node.window == None else "window", 258 | workspaceData.slaveMark)) 259 | if(node.window != None): 260 | self.i3.command('[con_id=%s] focus' % (node.id)) 261 | 262 | self.getMasterSize() 263 | pass 264 | 265 | def on_new(self, event): 266 | workspace = self.i3.get_tree().find_focused().workspace() 267 | workspaceData = self.getWorkSpaceData(workspace.num) 268 | if workspaceData.isDisable: 269 | return 270 | window = self.i3.get_tree().find_focused() 271 | 272 | # print("NEW ===============") 273 | # pprint(vars(workspaceData)) 274 | # print(window.parent.ipc_data) 275 | # dumpWorkSpace(workspace.ipc_data) 276 | if ( 277 | len(workspace.nodes) == 1 and 278 | len(workspace.nodes[0].nodes) == 0 and 279 | window.name == self.config.terminal and 280 | len(workspace.floating_nodes) == 0 281 | ): 282 | workspaceData.masterWidth = 0 283 | workspaceData.firstWindowId = window.id 284 | event.container.command('floating enable') 285 | event.container.command( 286 | "exec xdotool windowsize %d %s %s;exec xdotool windowmove %d %s %s" 287 | % (window.window, self.config.screenWidth, self.config.screenHeight, window.window, self.config.posX, self.config.posY)) 288 | 289 | if ( 290 | workspaceData.firstWindowId != 0 and 291 | window.floating == "auto_off" and 292 | len(workspace.floating_nodes) >= 1 and 293 | len(workspace.floating_nodes[0].nodes) >= 1 and 294 | len(workspace.nodes) == 1 and 295 | len(workspace.nodes[0].nodes) == 0 296 | ): 297 | # if seconde node open it change first node to tiling mode 298 | firstNode = self.findChildNodeById( 299 | workspace.floating_nodes, workspaceData.firstWindowId) 300 | if( 301 | firstNode != None and 302 | firstNode.id != window.id and 303 | ## only auto change on terminal instance 304 | firstNode.ipc_data["window_properties"]["instance"] == self.config.terminal 305 | ): 306 | firstWindowId = firstNode.id 307 | self.i3.command('[con_id=%s] floating disable' % firstWindowId) 308 | self.i3.command('[con_id=%s] move left' % firstWindowId) 309 | self.i3.command('[con_id=%s] mark %s' % ( 310 | firstWindowId, self.getWorkSpaceMark(masterMark, workspace.num))) 311 | if (self.config.firstScreenPercent > 0): 312 | self.i3.command('[con_id=%s] resize grow width %s px or %s ppt ' 313 | % (firstWindowId, self.config.firstScreenPercent, self.config.firstScreenPercent)) 314 | event.container.command('split vertical') 315 | workspaceData.firstWindowId = 0 316 | pass 317 | 318 | if(self.isSwapMasterOnNewInstance): 319 | self.i3.command('[con_id=%s] mark %s' % 320 | (window.parent.id, workspaceData.rootMark)) 321 | self.swapMaster(event) 322 | pass 323 | # second node is automatic split vertical 324 | elif ( 325 | len(window.parent.nodes) == 2 and 326 | window.parent.layout == 'splith' and 327 | workspaceData.rootMark not in window.parent.marks 328 | ): 329 | event.container.command('split vertical') 330 | pass 331 | 332 | # swap master and push master to top of stack of slave nodes 333 | if self.isSwapMasterOnNewInstance: 334 | isRootParent= workspaceData.rootMark in window.parent.marks 335 | masterNode = self.findChildNodeByMarked( 336 | workspace, workspaceData.masterMark) 337 | if self.isSwallowNext: 338 | self.isSwallowNext = False 339 | if(masterNode!=None): 340 | print("resizeMaster") 341 | self.resizeMaster(masterNode.id) 342 | isRootParent = False 343 | pass 344 | if(isRootParent): 345 | slaveNode = self.findChildNodeByMarked( 346 | workspace, workspaceData.slaveMark) 347 | if(masterNode != None and masterNode.id != window.id): 348 | if(slaveNode != None and len(slaveNode.nodes)>0): 349 | # push to slave stack 350 | firstNode = slaveNode.nodes[0] 351 | self.i3.command('[con_id=%s] focus' % 352 | (firstNode.id)) 353 | self.i3.command('[con_id=%s] move window to mark %s' % ( 354 | masterNode.id, workspaceData.slaveMark)) 355 | self.i3.command('[con_id=%s] swap container with con_id %d' 356 | % (masterNode.id, firstNode.id)) 357 | pass 358 | else: 359 | # no slave stack 360 | self.i3.command('[con_id=%s] mark %s' % 361 | (masterNode.id, workspaceData.slaveMark)) 362 | if len(window.parent.nodes)>0: 363 | self.i3.command('[con_id=%s] swap container with con_id %d' 364 | % (masterNode.id, window.id)) 365 | self.i3.command('[con_id=%s] move left'% ( window.id)) 366 | 367 | 368 | self.i3.command('[con_id=%s] unmark %s' % 369 | (masterNode.id, workspaceData.masterMark)) 370 | self.i3.command('[con_id=%s] mark %s' % 371 | (window.id, workspaceData.masterMark)) 372 | if(workspaceData.masterWidth != 0): 373 | self.i3.command('[con_id=%s] resize set %s 0' 374 | % (window.id, workspaceData.masterWidth)) 375 | self.i3.command('[con_id=%s] focus' % (masterNode.id)) 376 | self.i3.command('[con_id=%s] focus' % (window.id)) 377 | workspaceData.swapNodeId = masterNode.id 378 | pass 379 | return 380 | pass 381 | 382 | self.validateMasterAndSlaveNode(workspace) 383 | pass 384 | 385 | def gotoMaster(self, event): 386 | window = self.i3.get_tree().find_focused() 387 | workspace = self.i3.get_tree().find_focused().workspace() 388 | workspaceData = self.getWorkSpaceData(workspace.num) 389 | masterNode = self.findChildNodeByMarked( 390 | workspace, workspaceData.masterMark) 391 | if(masterNode != None): 392 | lastSwapNodeId = workspaceData.swapNodeId 393 | if(lastSwapNodeId != 0): 394 | isInMaster = masterNode.window != None and ( 395 | workspaceData.masterMark in window.marks) 396 | if(isInMaster == False): 397 | childs = self.getAllChildWindow(masterNode) 398 | for node in childs: 399 | if(window.id == node.id): 400 | isInMaster = True 401 | break 402 | pass 403 | if(isInMaster): 404 | self.i3.command('[con_id=%s] focus' % 405 | (lastSwapNodeId)) 406 | workspace.swapNodeId = 0 407 | return 408 | pass 409 | if(masterNode.window != None): 410 | self.i3.command('[con_id=%s] focus' % (masterNode.id)) 411 | workspaceData.swapNodeId = window.id 412 | pass 413 | if(len(masterNode.nodes) > 0 and masterNode.nodes[0].window != None): 414 | self.i3.command('[con_id=%s] focus' % (masterNode.nodes[0].id)) 415 | workspaceData.swapNodeId = window.id 416 | pass 417 | pass 418 | 419 | def swap2Node(self, node1Id: int, node2Id: int, workspaceData: WorkspaceData): 420 | self.i3.command('[con_id=%s] swap container with con_id %s' % 421 | (node1Id, node2Id)) 422 | self.i3.command('[con_id=%s] unmark %s' % 423 | (node1Id, workspaceData.masterMark)) 424 | self.i3.command('[con_id=%s] mark --add %s' % 425 | (node2Id, workspaceData.masterMark)) 426 | self.i3.command('[con_id=%s] focus' % (node2Id)) 427 | workspaceData.swapNodeId = node1Id 428 | self.emmit('master_change', node2Id) 429 | 430 | def swapMaster(self, event): 431 | window = self.i3.get_tree().find_focused() 432 | workspace = self.i3.get_tree().find_focused().workspace() 433 | workspaceData = self.getWorkSpaceData(workspace.num) 434 | masterNode = self.findChildNodeByMarked( 435 | workspace, workspaceData.masterMark) 436 | if(masterNode != None): 437 | lastSwapNodeId = workspaceData.swapNodeId 438 | if(self.config.limitWindowOnMaster == 1 or len(masterNode.nodes) == 0): 439 | if(lastSwapNodeId != 0 and workspaceData.masterMark in window.marks): 440 | self.swap2Node( 441 | masterNode.id, lastSwapNodeId, workspaceData) 442 | pass 443 | else: 444 | self.swap2Node(masterNode.id, window.id, 445 | workspaceData) 446 | else: 447 | # multi child in master 448 | childs = self.getAllChildWindow(masterNode) 449 | isInMaster = False 450 | for node in childs: 451 | if(window.id == node.id): 452 | isInMaster = True 453 | break 454 | if(isInMaster): 455 | if(lastSwapNodeId != 0): 456 | self.swap2Node( 457 | window.id, lastSwapNodeId, workspaceData) 458 | # workspaceData.swapNodeId = 0 459 | else: 460 | for node in childs: 461 | if(node.id != window.id): 462 | self.swap2Node( 463 | window.id, node.id, workspaceData) 464 | break 465 | else: 466 | if(len(childs) > 0 and childs[0].window != None): 467 | masterNode = childs[0] 468 | pass 469 | self.swap2Node(masterNode.id, window.id, workspaceData) 470 | 471 | pass 472 | 473 | def getMasterSize(self): 474 | window = self.i3.get_tree().find_focused() 475 | workspace = window.workspace() 476 | workspaceData = self.getWorkSpaceData(workspace.num) 477 | if ( 478 | workspaceData.masterMark in window.marks and 479 | workspaceData.rootMark in window.parent.marks and 480 | len(window.parent.nodes) == 2 481 | ): 482 | workspaceData.masterWidth = int(window.rect.width) 483 | pass 484 | 485 | def resizeMaster(self, condId: int): 486 | window = self.i3.get_tree().find_focused() 487 | workspace = window.workspace() 488 | workspaceData = self.getWorkSpaceData(workspace.num) 489 | if(workspaceData.masterWidth>0): 490 | self.i3.command('[con_id=%s] resize set %s 0' 491 | % (condId, workspaceData.masterWidth)) 492 | pass 493 | # region Event Handler 494 | def on(self, event_name, callback): 495 | if self.callbacks is None: 496 | self.callbacks = {} 497 | 498 | if event_name not in self.callbacks: 499 | self.callbacks[event_name] = [callback] 500 | else: 501 | self.callbacks[event_name].append(callback) 502 | 503 | def emmit(self, event_name, data=None): 504 | if self.callbacks is not None and event_name in self.callbacks: 505 | for callback in self.callbacks[event_name]: 506 | callback(data) 507 | # endregion 508 | 509 | def on_close(self, event): 510 | workspace = self.i3.get_tree().find_focused().workspace() 511 | workspaceData = self.getWorkSpaceData(workspace.num) 512 | if(workspaceData.isDisable): 513 | return 514 | allChild=workspace.leaves() 515 | isCloseMaster = False 516 | if(workspaceData.masterMark in event.container.marks): 517 | isCloseMaster = True 518 | self.validateMasterAndSlaveNode(workspace) 519 | if(isCloseMaster): 520 | focusWindow = self.i3.get_tree().find_focused() 521 | if(focusWindow != None and focusWindow.window != None): 522 | self.i3.command('[con_id=%s] move left' % (focusWindow.id)) 523 | self.i3.command('[con_id=%s] mark %s' % 524 | (focusWindow.id, workspaceData.masterMark)) 525 | if(workspaceData.masterWidth != 0): 526 | self.i3.command('[con_id=%s] resize set %s 0' 527 | % (focusWindow.id, workspaceData.masterWidth)) 528 | else: 529 | print("focus window null") 530 | 531 | if(len(allChild)==1): 532 | self.i3.command('[con_id=%s] mark %s' % (allChild[0].id,workspaceData.masterMark)) 533 | self.i3.command('[con_id=%s] mark %s' % (allChild[0].parent.id,workspaceData.rootMark)) 534 | 535 | pass 536 | 537 | def on_move(self, event): 538 | pass 539 | 540 | def on_binding(self, event): 541 | workspace = self.i3.get_tree().find_focused().workspace() 542 | workspaceData= self.getWorkSpaceData(workspace.num) 543 | command = event.ipc_data["binding"]["command"].strip() 544 | if(command == "nop swap master"): 545 | self.swapMaster(event) 546 | elif(command == "nop master toggle"): 547 | workspaceData.isDisable = not workspaceData.isDisable 548 | elif(command == "nop go master"): 549 | self.gotoMaster(event) 550 | elif("resize" in event.ipc_data["binding"]["command"]): 551 | self.getMasterSize() 552 | elif(self.debug): 553 | if event.ipc_data["binding"]["command"] == "nop debug": 554 | workspace = i3.get_tree().find_focused().workspace() 555 | dumpWorkSpace(workspace.ipc_data) 556 | 557 | if(workspaceData.isDisable): 558 | return 559 | self.validateMasterAndSlaveNode(workspace) 560 | pass 561 | 562 | pass 563 | 564 | def on_tick(self, event): 565 | 566 | self 567 | 568 | def on_focus(self, event): 569 | 570 | self 571 | 572 | # End class 573 | 574 | 575 | i3 = i3ipc.Connection() 576 | 577 | listHandler = [] 578 | masterConfig= I3MasterConfig() 579 | 580 | 581 | def on_close(self, event): 582 | for handler in (listHandler): 583 | handler.on_close(event) 584 | pass 585 | 586 | def on_floating (self,event): 587 | for handler in (listHandler): 588 | handler.on_close(event) 589 | pass 590 | 591 | def on_new(self, event): 592 | for handler in listHandler: 593 | handler.on_new(event) 594 | pass 595 | 596 | 597 | def on_move(self, event): 598 | for handler in listHandler: 599 | handler.on_move(event) 600 | pass 601 | 602 | 603 | def on_focus(self, event): 604 | for handler in listHandler: 605 | handler.on_focus(event) 606 | pass 607 | 608 | 609 | def on_binding(self, event): 610 | for handler in listHandler: 611 | handler.on_binding(event) 612 | pass 613 | 614 | 615 | def on_tick(self, event): 616 | for handler in listHandler: 617 | handler.on_tick(event) 618 | 619 | 620 | def main(): 621 | global listHandler 622 | global masterConfig 623 | parser = argparse.ArgumentParser() 624 | parser.add_argument( 625 | '--debug', 626 | action='store_true', 627 | help='Print debug messages to stderr' 628 | ) 629 | args = parser.parse_args() 630 | 631 | masterHander = I3MasterLayout(i3, masterConfig, args.debug) 632 | swallowHander = i3_swallow.I3Swallow( 633 | i3, masterConfig.isEnableSwallow, masterMark, masterHander) 634 | if(masterConfig.isEnableSwallow): 635 | listHandler.append(swallowHander) 636 | listHandler.append(masterHander) 637 | # Subscribe to events 638 | 639 | i3.on("window::new", on_new) 640 | i3.on("window::focus", on_focus) 641 | i3.on("window::close", on_close) 642 | i3.on("window::move", on_move) 643 | i3.on("binding", on_binding) 644 | i3.on("tick", on_tick) 645 | i3.main() 646 | 647 | def readConfig(): 648 | config_path = '%s/.config/i3/i3_master.ini' % os.environ['HOME'] 649 | dir = os.path.dirname(os.path.realpath(__file__)) 650 | if not os.path.isfile(config_path): 651 | print("No config file in.") 652 | # copy file 653 | shutil.copy(dir+"/i3_master.ini", config_path) 654 | pass 655 | config = configparser.ConfigParser() 656 | config.read(config_path) 657 | global masterConfig 658 | configData = config['config'] 659 | if(configData!=None): 660 | masterConfig.terminal = configData.get( 661 | 'terminal', fallback=masterConfig.terminal) 662 | masterConfig.posX = configData.getint( 663 | 'posX', fallback=masterConfig.posX) 664 | masterConfig.posY = configData.getint( 665 | 'posY', fallback=masterConfig.posY) 666 | masterConfig.screenWidth = configData.getint( 667 | 'screenWidth', fallback=masterConfig.screenWidth) 668 | masterConfig.screenHeight = configData.getint( 669 | 'screenHeight', fallback=masterConfig.screenHeight) 670 | masterConfig.isEnableSwallow = configData.getboolean( 671 | 'swallow', fallback=masterConfig.isEnableSwallow) 672 | masterConfig.isSwapMasterOnNewInstance = configData.getboolean( 673 | 'slaveStack', fallback=masterConfig.isEnableSwallow) 674 | masterConfig.firstScreenPercent = configData.getint( 675 | 'masterSizePlus', fallback=14) 676 | masterConfig.limitWindowOnMaster = configData.get( 677 | 'limitWindowOnMaster', fallback=masterConfig.limitWindowOnMaster) 678 | pass 679 | 680 | if __name__ == "__main__": 681 | readConfig() 682 | main() 683 | -------------------------------------------------------------------------------- /i3_swallow.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | #----------------------------------------------- 3 | # used to swallow a terminal window in i3 4 | # this process is check automatic in i3 5 | # When i3 have new window it will check parent process 6 | # and if it find a parent process have same id in current workspace it will swallow it 7 | #---------------------------------------------------- 8 | 9 | import i3ipc 10 | import subprocess 11 | from time import sleep 12 | from pprint import pprint 13 | import i3_master_layout 14 | 15 | 16 | class I3Swallow(object): 17 | def __init__(self, i3, terminal, masterTag, masterHander: i3_master_layout.I3MasterLayout): 18 | self.i3 = i3 19 | self.terminal = terminal 20 | self.masterTag = masterTag 21 | self.masterHandler = masterHander 22 | self.swallowDict = {} 23 | self.nextSwallowId = 0 24 | self.masterHandler.on("master_change", self.on_master) 25 | pass 26 | 27 | def unMarkAllNode(self, node, marked): 28 | for mark in node.marks: 29 | if mark == marked: 30 | self.i3.command('[con_id=%s] unmark %s' % (node.id, marked)) 31 | return True 32 | for node in node.nodes: 33 | if(self.unMarkAllNode(node, marked)): 34 | return True 35 | return False 36 | 37 | def checkNodeIsMater(self, node): 38 | if(self.masterTag != None): 39 | for mark in node.marks: 40 | if(mark.startswith(self.masterTag)): 41 | return True 42 | return False 43 | 44 | def hideSwallowParent(self, node, windowId, swallow): 45 | if(str(node.window) == str(windowId)): 46 | self.i3.command('[con_id=%s] mark --add %s' % 47 | (node.parent.id, "swallow"+str(node.id))) 48 | self.i3.command('[con_id=%s] move to scratchpad' % node.id) 49 | # check use master layout 50 | isMaster = self.checkNodeIsMater(node) 51 | 52 | self.i3.command('[con_id=%s] focus' % swallow.id) 53 | self.swallowDict[str(swallow.id)] = { 54 | "id": node.id, 55 | "layout": node.layout, 56 | "index": node.parent.nodes.index(node), 57 | "isMaster": isMaster, 58 | # minus to hidden window, 59 | "parent_nodes": len(node.parent.nodes)-1, 60 | } 61 | 62 | self.masterHandler.isSwallowNext = True 63 | if(isMaster == True): 64 | self.masterHandler.resizeMaster(swallow.id) 65 | return True 66 | for node in node.nodes: 67 | if(self.hideSwallowParent(node, windowId, swallow)): 68 | return True 69 | return False 70 | 71 | def getParentNodePid(self, node): 72 | # get parent of pid because terminal spwan shell(zsh or fish) and then spawn that child process 73 | output = subprocess.getoutput( 74 | "ps -o ppid= -p $(ps -o ppid= -p $(xprop -id %d _NET_WM_PID | cut -d' ' -f3 ))" % (node.window)) 75 | return output 76 | 77 | def getWindowIdfromPId(self, pid): 78 | output = subprocess.getoutput("xdotool search -pid %s" % pid) 79 | return output 80 | 81 | def on_master(self, newMasterId): 82 | for key in self.swallowDict: 83 | item = self.swallowDict.get(key) 84 | if(item["id"] == newMasterId): 85 | item["isMaster"] = True 86 | else: 87 | item["isMaster"] = False 88 | pass 89 | 90 | def on_new(self, event): 91 | if event.container.name != self.terminal: 92 | workspace = self.i3.get_tree().find_focused().workspace() 93 | if(self.nextSwallowId != 0): 94 | parentContainer = workspace.find_by_window(self.nextSwallowId) 95 | if(parentContainer != None): 96 | self.hideSwallowParent( 97 | parentContainer, self.nextSwallowId, event.container) 98 | self.nextSwallowId = 0 99 | return 100 | 101 | self.nextSwallowId = 0 102 | # if we can find parent node have pid map to any node in workspace we will hide it 103 | # TODO change it to check class name of this application and if that class name belong to a list of swallow name then we will swallow it 104 | # the process for check parent pid is slow 105 | parentContainerPid = self.getParentNodePid(event.container) 106 | #id of root 107 | if(parentContainerPid != " 1" and len(parentContainerPid) < 9): 108 | parentContainerWid = self.getWindowIdfromPId( 109 | parentContainerPid) 110 | for item in workspace.nodes: 111 | self.hideSwallowParent( 112 | item, parentContainerWid, event.container) 113 | pass 114 | 115 | def on_close(self, event): 116 | swallow = self.swallowDict.get(str(event.container.id)) 117 | if swallow != None: 118 | workspace = self.i3.get_tree().find_focused().workspace() 119 | window = self.i3.get_tree().find_by_id(swallow["id"]) 120 | if window != None: 121 | mark = "swallow"+str(swallow['id']) 122 | del self.swallowDict[str(event.container.id)] 123 | self.i3.command( 124 | '[con_id=%s] scratchpad show;floating disable;focus' % (window.id)) 125 | # try to restore to the original position 126 | if(swallow['isMaster'] == False): 127 | self.i3.command( 128 | '[con_id=%s] move container to mark %s' % (window.id, mark)) 129 | parentMarked = workspace.find_marked(mark) 130 | targetWindow = None 131 | if(len(parentMarked) > 0): 132 | self.i3.command('[con_id=%s] unmark %s' % 133 | (parentMarked[0].id, mark)) 134 | 135 | if(targetWindow == None and len(parentMarked) > 0 and len(parentMarked[0].nodes) > 0): 136 | if (swallow["index"] < len(parentMarked[0].nodes)): 137 | targetWindow = parentMarked[0].nodes[swallow['index']] 138 | 139 | if(targetWindow != None): 140 | self.i3.command('[con_id=%s] swap container with con_id %d' % ( 141 | window.id, targetWindow.id)) 142 | else: 143 | # can't find a good position for it let i3 handler 144 | pass 145 | 146 | self.i3.command('[con_id=%s] focus' % (window.id)) 147 | pass 148 | 149 | def on_move(self, event): 150 | swallow = self.swallowDict.get(str(event.container.id)) 151 | if swallow != None: 152 | focusWindow = self.i3.get_tree().find_focused() 153 | if focusWindow != None: 154 | mark = "swallow"+str(swallow['id']) 155 | self.unMarkAllNode(focusWindow.root(), mark) 156 | self.i3.command('[con_id=%s] mark --add %s' % 157 | (focusWindow.parent.id, mark)) 158 | swallow["layout"] = focusWindow.layout 159 | swallow["index"] = focusWindow.parent.nodes.index(focusWindow) 160 | swallow["parent_nodes"] = len(focusWindow.parent.nodes) 161 | swallow["isMaster"] = self.checkNodeIsMater(focusWindow) 162 | pass 163 | 164 | def on_binding(self, event): 165 | 166 | pass 167 | 168 | def on_focus(self, event): 169 | 170 | pass 171 | 172 | def on_tick(self, event): 173 | args = event.payload.split(' ') 174 | if(len(args) == 2 and args[0] == 'swallow'): 175 | try: 176 | self.nextSwallowId = int(args[1], 16) 177 | except Exception as e: 178 | print("id not valid %s" % args[1]) 179 | pass 180 | -------------------------------------------------------------------------------- /rofi-ag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # use AG search with rofi 3 | # 4 | #------------ CONFIG ----------------# 5 | # It support search text in symlink folder so you can add your symlink to this folder 6 | SEARCH_DIRECTORY="$HOME/Desktop" 7 | 8 | DIRECTORY_SHORTCUT=( 9 | "~/Downloads" 10 | "~/Documents" 11 | ) 12 | 13 | OPENER=xdg-open 14 | # load terminal with zsh shell. 15 | # I need load shell because my nodejs config and pywall theme for vifm :) 16 | # change to kitty by `kitty -e` 17 | TERM_SHEL="alacritty -e /bin/zsh -i -c" 18 | TEXT_EDITOR=nvim 19 | # change to ranger or lf or vifm i need it open in tmux 20 | FILE_MANAGER="$HOME/.config/my_scripts/m_tmux_fm.sh " 21 | 22 | VIM_OPEN_EXT=( 23 | "html" 24 | "md" 25 | "py" 26 | "go" 27 | "rb" 28 | "php" 29 | "lua" 30 | "sh" 31 | "cs" 32 | "txt" 33 | "ts" 34 | "js" 35 | "jsx" 36 | ) 37 | 38 | #------------ CONFIG ----------------# 39 | 40 | AG_TEXT_QUERY="--column --noheading --follow --depth 6" 41 | AG_FILE_QUERY='-g "" --follow' 42 | 43 | # MY_PATH="$(dirname "${0}")" 44 | TMP_DIR="/tmp/rofi/${USER}/" 45 | HIST_FILE="${TMP_DIR}/history.txt" 46 | 47 | 48 | if [ ! -d "${TMP_DIR}" ] 49 | then 50 | mkdir -p "${TMP_DIR}"; 51 | fi 52 | 53 | # Create hist file if it doesn't exist 54 | if [ ! -f "${HIST_FILE}" ] 55 | then 56 | touch "${HIST_FILE}" 57 | fi 58 | function mExit(){ 59 | exec 1>&- 60 | exit 1; 61 | } 62 | 63 | function searchAgText(){ 64 | isValid=0 65 | query=$@ 66 | printf -v search_text "%q\n" "$@" 67 | if [[ ${#query} -ge 3 ]]; then 68 | query="ag $search_text $AG_TEXT_QUERY $SEARCH_DIRECTORY " 69 | mapfile -t AG_RESULT < <(eval $query) 70 | index=1 71 | cat /dev/null > $HIST_FILE 72 | for s in "${AG_RESULT[@]}"; do 73 | if [[ ${#s} -ge 4 ]]; then 74 | printf -v j "%02d" $index 75 | COMMAND="$j:${s//$SEARCH_DIRECTORY/''}:t" 76 | echo $COMMAND >> $HIST_FILE 77 | echo $COMMAND 78 | index=$((index + 1)) 79 | isValid=1 80 | fi 81 | done 82 | fi 83 | 84 | if [[ isValid -eq 0 ]]; then 85 | echo "01:Not found:q" 86 | echo "01:Not found:q" >> $HIST_FILE 87 | return 0 88 | else 89 | return 0 90 | fi 91 | } 92 | function searchAgFile(){ 93 | query="find $SEARCH_DIRECTORY " 94 | mapfile -t AG_RESULT < <(eval $query) 95 | index=1 96 | cat /dev/null > $HIST_FILE 97 | for folder in "${DIRECTORY_SHORTCUT[@]}"; do 98 | printf -v j "%02d" $index 99 | COMMAND="$j:${folder}:a" 100 | echo $COMMAND >> $HIST_FILE 101 | echo $COMMAND 102 | index=$((index + 1)) 103 | done 104 | for s in "${AG_RESULT[@]}"; do 105 | if [[ ${#s} -ge ${#SEARCH_DIRECTORY}+3 ]]; then 106 | printf -v j "%02d" $index 107 | COMMAND="$j:${s//$SEARCH_DIRECTORY/''}:f" 108 | echo $COMMAND >> $HIST_FILE 109 | echo $COMMAND 110 | index=$((index + 1)) 111 | fi 112 | done 113 | } 114 | 115 | function checkNumber(){ 116 | re='^[0-9]+$' 117 | if ! [[ $@ =~ $re ]] ; then 118 | return 1 119 | fi 120 | return 0 121 | } 122 | 123 | function excute(){ 124 | readarray -t ARR < $HIST_FILE 125 | for s in "${ARR[@]}"; do 126 | if [[ "$1" == "${s:0:2}" ]]; then 127 | if [[ "$2" == "q" ]]; then 128 | exit 0 129 | elif [[ "$2" == "t" ]]; then 130 | IFS=':' read -r -a array <<< "$s" 131 | file=${array[1]} 132 | line=${array[2]} 133 | column=${array[3]} 134 | checkNumber $column 135 | isLineNumber=$? 136 | command="$TERM_SHEL '$TEXT_EDITOR \"+normal ${line}G${column}|\" $SEARCH_DIRECTORY${file}' " 137 | coproc (eval $command) 138 | mExit 139 | elif [[ "$2" == "f" ]]; then 140 | IFS=':' read -r -a array <<< "$s" 141 | fileOpen $SEARCH_DIRECTORY${array[1]} 142 | elif [[ "$2" == "a" ]]; then 143 | IFS=':' read -r -a array <<< "$s" 144 | fileOpen ${array[1]} 145 | else 146 | echo "01:not action:q" 147 | fi 148 | fi 149 | done 150 | } 151 | 152 | function fileOpen(){ 153 | file=$@ 154 | filename="${file##*/}" 155 | extension="${filename##*.}" 156 | if [[ "$filename" == "$extension" ]]; then 157 | coproc (eval "$TERM_SHEL '$FILE_MANAGER ${file}'" > /dev/null 2>&1 ) 158 | mExit 159 | elif [[ -x "$file" ]]; then 160 | coproc ($file> /dev/null 2>&1 ) 161 | mExit 162 | elif [[ " ${VIM_OPEN_EXT[@]} " =~ " ${extension} " ]]; then 163 | coproc ( eval "$TERM_SHEL '$TEXT_EDITOR ${file}'" > /dev/null 2>&1 ) 164 | mExit 165 | else 166 | coproc (eval "$OPENER ${file} " > /dev/null 2>&1 ) 167 | mExit 168 | fi 169 | } 170 | 171 | 172 | if [[ -z $@ ]]; then 173 | searchAgFile "" 174 | elif [[ "$@" == "quit" ]]; then 175 | exit 0 176 | elif [ "$@" == "--testt" ] 177 | then 178 | echo "Search text" 179 | read query 180 | searchAgText $query 181 | read query 182 | ./rofi-ag.sh $query 183 | elif [ "$@" == "--testf" ] 184 | then 185 | echo "Search file" 186 | searchAgFile "" 187 | read query 188 | ./rofi-ag.sh $query 189 | else 190 | query=$@ 191 | 192 | COMMAND=${query:0:3} 193 | last=${COMMAND:2:1} 194 | action="${query: -1}" 195 | if [[ $last == ":" ]]; then 196 | excute "${COMMAND:0:2}" $action 197 | else 198 | if [[ ${query:0:1} == "'" ]]; then 199 | searchAgText "${query:1}" 200 | else 201 | searchAgText "$query" 202 | fi 203 | fi 204 | fi 205 | -------------------------------------------------------------------------------- /screenshot/first_terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/windwp/i3-master-stack/0760adc390500a7abdfe44e8d651e2e7712398b1/screenshot/first_terminal.png -------------------------------------------------------------------------------- /screenshot/swallow_vifm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/windwp/i3-master-stack/0760adc390500a7abdfe44e8d651e2e7712398b1/screenshot/swallow_vifm.gif -------------------------------------------------------------------------------- /screenshot/swap_master.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/windwp/i3-master-stack/0760adc390500a7abdfe44e8d651e2e7712398b1/screenshot/swap_master.gif -------------------------------------------------------------------------------- /swallow: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | tid=$(xdo id) 3 | pid=0 4 | OPTIND=1 5 | while getopts ":p:" opt; do 6 | case ${opt} in 7 | p) # process option h 8 | pid=$OPTARG 9 | ;; 10 | :) 11 | echo "Invalid option: $OPTARG requires an argument" 1>&2 12 | ;; 13 | esac 14 | done 15 | shift $((OPTIND-1)) 16 | 17 | if [[ pid != 0 ]]; then 18 | tid=$pid 19 | fi 20 | i3-msg -t send_tick "swallow ${tid}" 21 | sleep 2 22 | # escapce special character on path 23 | printf -v var "%q\n" "$@" 24 | echo $var 25 | eval $var 26 | 27 | -------------------------------------------------------------------------------- /temp.txt: -------------------------------------------------------------------------------- 1 | find *.sh | entr rofi -modi "Global Search":"./rofi-ag.sh" -show "Global Search" \ -matching regex 2 | --------------------------------------------------------------------------------