├── img1.png ├── img2.png ├── img3.png ├── README.md └── partition /img1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/gpartition/HEAD/img1.png -------------------------------------------------------------------------------- /img2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/gpartition/HEAD/img2.png -------------------------------------------------------------------------------- /img3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/gpartition/HEAD/img3.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gpartition 2 | 3 | Below is an example of a layout. 4 | The user runs 5 | 6 | ``` 7 | partition --apply 8 | ``` 9 | 10 | Seemingly, `partition` identifies the set of windows with one Sublime Text and two terminals. This means that the exact same layout has been learned before. The user can let `partition` re-learn the layout by invoking 11 | 12 | ``` 13 | partition --learn 14 | ``` 15 | 16 | If you are struggling with getting the windows to line up, simply invoke 17 | 18 | ``` 19 | partition --quantize 20 | ``` 21 | 22 | which will snap the windows to a much sparser grid on the desktop. 23 | 24 | ![An example layout](img1.png) 25 | 26 | Here is another layout, consisting of a visible instance of Brave, one instance of Sublime Text and two terminals. This one has also been saved. 27 | 28 | ![Another example layout](img2.png) 29 | 30 | Now, the user ran 31 | 32 | ``` 33 | partition --pack 34 | ``` 35 | 36 | This takes the set of visible windows and packs them (with a gap) using a heuristic bin-packing algorithm. I did not bother to write my own, so the user needs to install it: 37 | 38 | ``` 39 | pip install rectpack 40 | ``` 41 | 42 | ![Packed windows](img3.png) 43 | -------------------------------------------------------------------------------- /partition: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | The MIT License (MIT) 5 | 6 | Copyright (c) 2019 Carl Londahl 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | """ 26 | 27 | import os 28 | import json 29 | import syslog 30 | import subprocess 31 | import argparse 32 | 33 | from rectpack import newPacker, PackingBin 34 | from hashlib import sha1 35 | 36 | # some graphical adjustments needed to make it 37 | # look properly 38 | y_border = 24 39 | x_border = 12 40 | screen = 3840 - 2 * x_border, 2160 - 3 * y_border - 80 41 | 42 | # this may be needed to be reconfigured 43 | xdotool_offset_error = 4, 58 44 | 45 | # in case we cannot pack the windows, the program 46 | # will identify some programs that can be minimized 47 | # to find a suitable packing. 48 | minimize_if_needed = ["spotify", "telegram"] 49 | 50 | # config directory 51 | home = os.path.expanduser("~") 52 | layouts_config = "%s/.config/gpartition/layouts.json" % home 53 | 54 | # for sending error somewhere 55 | devnull = open(os.devnull, 'wb') 56 | 57 | 58 | def notify(msg): 59 | subprocess.call(["notify-send", "Layouts", str(msg)]) 60 | 61 | 62 | def get_desktop(): 63 | return subprocess.check_output(["xdotool", "get_desktop"]).strip() 64 | 65 | 66 | def get_pos_and_size(id): 67 | result = subprocess.check_output(["xdotool", "getwindowgeometry", id]).decode("ascii") 68 | result = result.split("Position: ")[1].split("Geometry: ") 69 | x, y = result[0].split(" (")[0].split(",") 70 | width, height = result[1].strip().split("x") 71 | return int(x), int(y), int(width), int(height) 72 | 73 | 74 | def get_size(id): 75 | _, _, width, height = get_pos_and_size(id) 76 | return width, height 77 | 78 | 79 | def move_window(id, x, y): 80 | subprocess.call(["xdotool", "windowmove", str(id), str(x), str(y)]) 81 | 82 | 83 | def resize_window(id, width, height): 84 | width -= xdotool_offset_error[0] 85 | height -= xdotool_offset_error[1] 86 | subprocess.call( 87 | ["xdotool", "windowsize", str(id), str(width), str(height)]) 88 | 89 | 90 | def get_windows(desktop): 91 | return subprocess.check_output([ 92 | "xdotool", "search", "--all", "--onlyvisible", "--desktop", get_desktop(), "" 93 | ], stderr=devnull).decode("ascii").split() 94 | 95 | 96 | def get_name(id): 97 | return subprocess.check_output( 98 | ["xprop", "-id", str(id), "WM_CLASS"] 99 | ).decode("ascii").strip() 100 | 101 | 102 | def minimize(id): 103 | subprocess.call(["xdotool", "windowminimize", str(id)]) 104 | 105 | 106 | def find_minimizable(): 107 | for window_class in minimize_if_needed: 108 | try: 109 | return subprocess.check_output([ 110 | "xdotool", "search", "--onlyvisible", "--class", window_class 111 | ]).strip().split() 112 | except Exception: 113 | continue 114 | return list() 115 | 116 | 117 | def get_window_properties(visible_windows): 118 | windows = [] 119 | for window in visible_windows: 120 | x, y, width, height = get_pos_and_size(window) 121 | windows.append( 122 | (sha1(get_name(window)).hexdigest(), x, y, width, height)) 123 | 124 | digests = sorted(map(lambda x: x[0], windows)) 125 | fingerprint = sha1("".join(digests)).hexdigest() 126 | return windows, fingerprint 127 | 128 | 129 | def learn_layout(visible_windows): 130 | windows, fingerprint = get_window_properties(visible_windows) 131 | try: 132 | with open(layouts_config, "r") as layouts: 133 | data = json.loads(layouts.read()) 134 | except Exception: 135 | data = dict() 136 | try: 137 | data[fingerprint] = windows 138 | with open(layouts_config, "w+") as layouts: 139 | layouts.write(json.dumps(data)) 140 | notify("Layout saved") 141 | except Exception: 142 | syslog.syslog(syslog.LOG_ERR, "Failed to write to %s." % 143 | layouts_config) 144 | 145 | 146 | def apply_layout(visible_windows): 147 | windows, fingerprint = get_window_properties(visible_windows) 148 | try: 149 | with open(layouts_config, "r") as layouts: 150 | data = json.loads(layouts.read()) 151 | layout = data.get(fingerprint) 152 | assert(layout is not None) 153 | except Exception: 154 | return 155 | 156 | visible_windows = get_windows(get_desktop()) 157 | for window in visible_windows: 158 | window_fingerprint = sha1(get_name(window)).hexdigest() 159 | # a bit inefficient, but we are not dealing with to many windows 160 | e = filter(lambda x: x[0] == window_fingerprint, layout)[0] 161 | layout.remove(e) 162 | move_window(window, e[1], e[2]) 163 | resize_window(window, e[3], e[4]) 164 | 165 | 166 | def pack(visible_windows): 167 | # create a representation of the screen and 168 | # init the bin-packing algorithm. 169 | packer = newPacker(rotation=False, bin_algo=PackingBin.BFF) 170 | packer.add_bin(*screen) 171 | 172 | # determine the visisble windows on the desktop 173 | for window in visible_windows: 174 | width, height = get_size(window) 175 | dimension = (width + 3 * x_border, height + 4 * y_border) 176 | packer.add_rect(*dimension, rid=window) 177 | 178 | # try to perform packing of the windows 179 | packer.pack() 180 | 181 | # if we did not succeed, we will try to minimize 182 | # some windows that are allowed to be minized. 183 | while len(packer[0]) != len(packer._avail_rect): 184 | minimizable_windows = find_minimizable() 185 | # if no minimizable windows are found, we 186 | # need not to attempt further. 187 | if len(minimizable_windows) == 0: 188 | break 189 | # pick a window in the list and 190 | # minimize it. 191 | for window in minimizable_windows: 192 | g = filter(lambda x: x[2] == window, list(packer._avail_rect)) 193 | packer._avail_rect.remove(g[0]) 194 | minimize(g[0][2]) 195 | packer.pack() 196 | 197 | # get bounding box 198 | mx, my = -1, -1 199 | for p in packer[0]: 200 | mx = max(p.x + p.width, mx) 201 | my = max(p.y + p.height, my) 202 | 203 | # center bounding box 204 | dx = (screen[0] - mx) / 2 205 | dy = (screen[1] - my) / 2 206 | 207 | # apply final transformation to desktop 208 | for p in packer[0]: 209 | move_window(p.rid, p.x + 3 * x_border + dx, p.y + 4 * y_border + dy) 210 | 211 | 212 | def quantize(visible_windows): 213 | def normalize(num, c): 214 | return round(float(num) / border / c) * border * c 215 | 216 | for window in visible_windows: 217 | x, y, width, height = get_pos_and_size(window) 218 | x = normalize(x, 3) - xdotool_offset_error[0] 219 | y = normalize(y, 3) - xdotool_offset_error[1] 220 | width = normalize(width, 3) 221 | height = normalize(height, 3) 222 | move_window(window, x, y) 223 | resize_window(window, width, height) 224 | 225 | parser = argparse.ArgumentParser(prog='gtile') 226 | parser.add_argument('--learn', action='store_true', 227 | help='learn current layout') 228 | parser.add_argument('--apply', action='store_true', 229 | help='apply layout to current windows') 230 | parser.add_argument('--pack', action='store_true', 231 | help='attempt to pack windows') 232 | parser.add_argument('--quantize', action='store_true', 233 | help='attempt to quantize windows') 234 | args = parser.parse_args() 235 | 236 | visible_windows = get_windows(get_desktop()) 237 | 238 | if args.learn: 239 | learn_layout(visible_windows) 240 | if args.apply: 241 | apply_layout(visible_windows) 242 | if args.pack: 243 | pack(visible_windows) 244 | if args.quantize: 245 | quantize(visible_windows) 246 | 247 | # TODO: make the learning of layouts a bit more flexible. this can be achieved using 248 | # a proper machine-learning algorithm such as a neural net. 249 | --------------------------------------------------------------------------------