├── .gitignore ├── COPYING ├── MANIFEST.in ├── Makefile ├── PKGBUILD ├── README.rst ├── quickswitch.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.json 3 | *.tar.* 4 | */* 5 | MANIFEST 6 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | 15 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include COPYING 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean 2 | clean: 3 | # This will remove EVERYTHING that's not tracked by git. Use with care. 4 | git clean -df 5 | 6 | .PHONY: dist 7 | dist: 8 | python setup.py sdist upload 9 | 10 | .PHONY: arch-source 11 | arch-source: 12 | makepkg --source 13 | -------------------------------------------------------------------------------- /PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: slowpoke 2 | pkgname='quickswitch-i3' 3 | pkgver=2.2 4 | pkgrel=1 5 | pkgdesc="quickly change to and locate windows in i3" 6 | arch=(any) 7 | url="https://github.com/proxypoke/quickswitch-for-i3" 8 | license=('WTFPL') 9 | groups=() 10 | depends=('i3-wm' 'python' 'i3-py-git' 'dmenu') 11 | makedepends=() 12 | provides=() 13 | conflicts=() 14 | replaces=() 15 | backup=() 16 | options=(!emptydirs) 17 | install= 18 | source=("http://pypi.python.org/packages/source/q/quickswitch-i3/quickswitch-i3-${pkgver}.tar.gz") 19 | md5sums=('92ea72936ae4879b002e50ae165b1457') 20 | 21 | package() { 22 | cd "$srcdir/$pkgname-$pkgver" 23 | python setup.py install --root="$pkgdir/" --optimize=1 24 | } 25 | 26 | # vim:set ts=2 sw=2 et: 27 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | quickswitch for i3 2 | ================== 3 | 4 | IMPORTANT NOTICE 5 | ---------------- 6 | 7 | I have given up maintenance of this project a long time ago, since I stopped 8 | working on it due to personal reasons. Some other i3 enthusiasts have since 9 | then taken up the project and continued its development and maintenance, so 10 | if you would like to contribute or report issues, please head towards 11 | `this repository `_. I truly 12 | appreciate that people have decided to continue this, many thanks to them. 13 | 14 | Overview 15 | -------- 16 | This utility for i3_, inspired by Pentadactyl_'s ``:buffers`` command, allows 17 | you to quickly switch to and locate windows on all your workspaces, using an 18 | interactive dmenu prompt. It has since gained a lot of other functionality to 19 | make working with i3 even more efficient. 20 | 21 | Usage 22 | ----- 23 | Finding windows 24 | ~~~~~~~~~~~~~~~ 25 | 26 | The core functionality of quickswitch is still finding windows and jumping to 27 | them, and this is what it does when you call it without any options. 28 | 29 | Here's how it looks in action: 30 | 31 | .. image:: http://i.imgur.com/QeQrM.png 32 | 33 | However, sometimes you may want to grab a window and move it to your current 34 | workspace. This can be done with the ``-m/--move`` flag. 35 | 36 | A similiar feature is the ``-s/--scratchpad`` flag, which searches your 37 | scratchpad, and does a ``scratchpad show`` on the window you choose. 38 | 39 | You can also search and jump (or move) via regular expression using the 40 | ``-r``/``--regex`` flag, without using dmenu. This could be useful for 41 | scripting, or if you are a regex wizard who feels limited by dmenu. 42 | 43 | Workspaces 44 | ~~~~~~~~~~ 45 | 46 | quickswitch also provides a few functions to manage workspaces. First of 47 | all, it allows you to search workspaces in the same fashion as windows with the 48 | ``-w/--workspaces`` flag. This is *extremely* useful for working with many named 49 | workspaces without having them bound to any particular key. 50 | 51 | Another useful feature is to quickly get an empty workspace. This is what the 52 | ``-e/--empty`` flag does: it will jump you to the first empty, numbered 53 | workspace. 54 | 55 | If you use this excessively, then your numbered workspaces might fragment a lot. 56 | You can fix this easily with ``-g``/``--degap``, which "defragments" your 57 | workspaces, without affecting their order (eg, [1, 4, 7] becomes [1, 2, 3] by 58 | renaming 4 to 2 and 7 to 3). 59 | 60 | While on numbered workspaces, it can be pretty handy to jump to the next or 61 | previous numbered workspace ("cycle" them). ``-p/--previous`` and ``-n/--next`` 62 | do just that. What's more, you can combine them with the ``-m/--move`` flag to 63 | move the currently focused container to the respective workspace instead. Note 64 | that ``--previous`` will happily go to negative workspace numbers, which are 65 | then no longer treated as numbered by i3 (ie, they won't get sorted, like named 66 | workspaces). 67 | 68 | dmenu 69 | ~~~~~ 70 | 71 | You can influence how dmenu is called with the ``-d/--dmenu`` flag, which 72 | expects a complete dmenu command. The default is ``dmenu -b -i -l 20`` (which 73 | makes dmenu appear on the bottom of your screen (-b) in a vertical manner with 74 | at most 20 lines (-l 20), and matches case insensitively (-i). See the man page 75 | for dmenu for a list of options. 76 | 77 | **Note:** The versions of quickswitch before 2.0 used explicit flags for changing 78 | dmenu's behavior. This was rather inflexible, because it needed an explicit flag 79 | for every dmenu option, and it hardcoded the dmenu command. For most people, the 80 | default should be fine, but if you want to change anything, this allows you to 81 | go wild. 82 | 83 | Dependencies 84 | ------------ 85 | quickswitch-i3 requires dmenu (which you likely already have installed), and 86 | i3-py, which you can install with ``pip install i3-py``. 87 | 88 | quickswitch-i3 was tested in Python 2.7.3 and 3.2.3. It will not work in version 89 | prior to 2.7 due to the usage of ``argparse``. 90 | 91 | Installation 92 | ------------ 93 | quickswitch-i3 has a PyPI entry, so you can install it with ``pip install 94 | quickswitch-i3``. Alternatively, you can always manually run the setup file with 95 | ``python setup.py install``. 96 | 97 | Additionally, if you are an Arch user, you can install it from the AUR. The 98 | package is called ``quickswitch-i3``. The PKGBUILD is also included here. 99 | 100 | **NOTE**: I do not maintain the AUR package anymore, since I do not have access 101 | to any Arch box. See comment on the AUR page. 102 | 103 | An overlay for Gentoo is in the works. 104 | 105 | Contributions 106 | ------------- 107 | ...are obviously welcome. Pretty much every feature in quickswitch originated 108 | because someone (not just me) thought "hey, this would be useful". Just shoot a 109 | Pull Request. 110 | 111 | License 112 | ------- 113 | **Disclaimer: quickswitch-i3 is a third party script and in no way affiliated 114 | with the i3 project.** 115 | 116 | This program is free software under the terms of the 117 | Do What The Fuck You Want To Public License. 118 | It comes without any warranty, to the extent permitted by 119 | applicable law. For a copy of the license, see COPYING or 120 | head to http://sam.zoy.org/wtfpl/COPYING. 121 | 122 | .. _Pentadactyl: http://5digits.org/pentadactyl/ 123 | .. _i3: http://i3wm.org 124 | -------------------------------------------------------------------------------- /quickswitch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # quickswitch for i3 - quickly change to and locate windows in i3. 4 | # 5 | # Author: slowpoke 6 | # This program is Free Software under the terms of the 7 | # 8 | # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 9 | # Version 2, December 2004 10 | # 11 | # Copyright (C) 2004 Sam Hocevar 12 | # 13 | # Everyone is permitted to copy and distribute verbatim or modified 14 | # copies of this license document, and changing it is allowed as long 15 | # as the name is changed. 16 | # 17 | # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 18 | # TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 19 | # 20 | # 0. You just DO WHAT THE FUCK YOU WANT TO. 21 | 22 | __version__ = '2.2' 23 | 24 | 25 | import argparse 26 | import subprocess 27 | import os 28 | import re 29 | 30 | try: 31 | import i3 32 | except ImportError: 33 | print("quickswitch requires i3-py.") 34 | print("You can install it from the PyPI with ``pip install i3-py''.") 35 | exit(1) 36 | 37 | 38 | def check_dmenu(): 39 | '''Check if dmenu is available.''' 40 | devnull = open(os.devnull) 41 | retcode = subprocess.call(["which", "dmenu"], 42 | stdout=devnull, 43 | stderr=devnull) 44 | return True if retcode == 0 else False 45 | 46 | 47 | def dmenu(options, dmenu): 48 | '''Call dmenu with a list of options.''' 49 | 50 | cmd = subprocess.Popen(dmenu, 51 | shell=True, 52 | stdin=subprocess.PIPE, 53 | stdout=subprocess.PIPE, 54 | stderr=subprocess.PIPE) 55 | stdout, _ = cmd.communicate('\n'.join(options).encode('utf-8')) 56 | return stdout.decode('utf-8').strip('\n') 57 | 58 | 59 | def get_windows(): 60 | '''Get all windows.''' 61 | windows = i3.filter(nodes=[]) 62 | return create_lookup_table(windows) 63 | 64 | 65 | def find_window_by_regex(regex, move=False): 66 | '''Find the first window whose title matches regex and focus or move it.''' 67 | action = move_window_here if move else focus 68 | 69 | cr = re.compile(regex) 70 | for title, id in get_windows().items(): 71 | if cr.match(title): 72 | action(id) 73 | return True 74 | return False 75 | 76 | 77 | def get_scratchpad(): 78 | '''Get all windows on the scratchpad.''' 79 | scratchpad = i3.filter(name="__i3_scratch")[0] 80 | nodes = scratchpad["floating_nodes"] 81 | windows = i3.filter(tree=nodes, nodes=[]) 82 | return create_lookup_table(windows) 83 | 84 | 85 | def get_workspaces(): 86 | '''Returns all workspace names. 87 | 88 | NOTE: This returns a map of name → name, which is rather redundant, but 89 | makes it possible to use the result without changing much in main(). 90 | ''' 91 | workspaces = i3.get_workspaces() 92 | for ws in workspaces: 93 | # create_lookup_table will set the value of all entries in the lookup table 94 | # to the window id. We act as if the workspace name is the window id. 95 | ws['window'] = ws['name'] 96 | return create_lookup_table(workspaces) 97 | 98 | 99 | def next_empty(): 100 | '''Return the lowest numbered workspace that is empty.''' 101 | workspaces = sorted([int(ws) for ws in get_workspaces().keys() 102 | if ws.isdecimal()]) 103 | for i in range(len(workspaces)): 104 | if workspaces[i] != i + 1: 105 | return str(i + 1) 106 | return str(len(workspaces) + 1) 107 | 108 | 109 | def next_used(number): 110 | '''Return the next used numbered workspace after the given number.''' 111 | workspaces = sorted([int(ws) for ws in get_workspaces().keys() 112 | if ws.isdecimal() 113 | and int(ws) > number]) 114 | return workspaces[0] if workspaces else None 115 | 116 | 117 | def degap(): 118 | '''Remove 'gaps' in the numbered workspaces. 119 | 120 | This searches for non-consecutive numbers in the workspace list, and moves 121 | used workspaces as far to the left as possible. 122 | 123 | ''' 124 | i = 0 125 | while True: 126 | ws = next_used(i) 127 | if ws is None: 128 | break 129 | elif ws - i > 1: 130 | rename_workspace(ws, i + 1) 131 | i += 1 132 | 133 | 134 | def create_lookup_table(windows): 135 | '''Create a lookup table from the given list of windows. 136 | 137 | The returned dict is in the format window title → X window id. 138 | ''' 139 | rename_nonunique(windows) 140 | lookup = {} 141 | for window in windows: 142 | name = window.get('name') 143 | id_ = window.get('window') 144 | if id_ is None: 145 | # this is not an X window, ignore it. 146 | continue 147 | if name.startswith("i3bar for output"): 148 | # this is an i3bar, ignore it. 149 | continue 150 | lookup[name] = id_ 151 | return lookup 152 | 153 | 154 | def rename_nonunique(windows): 155 | '''Rename all windows which share a name by appending an index.''' 156 | window_names = [window.get('name') for window in windows] 157 | for name in window_names: 158 | count = window_names.count(name) 159 | if count > 1: 160 | for i in range(count): 161 | index = window_names.index(name) 162 | window_names[index] = "{} [{}]".format(name, i + 1) 163 | for i in range(len(windows)): 164 | windows[i]['name'] = window_names[i] 165 | 166 | 167 | def get_scratchpad_window(window): 168 | '''Does `scratchpad show` on the specified window.''' 169 | return i3.scratchpad("show", id=window) 170 | 171 | 172 | def move_window_here(window): 173 | '''Does `move workspace current` on the specified window.''' 174 | return i3.msg(0, "{} move workspace current".format( 175 | i3.container(id=window))) 176 | 177 | 178 | def rename_workspace(old, new): 179 | '''Rename a given workspace.''' 180 | return i3.msg(0, "rename workspace {} to {}".format(old, new)) 181 | 182 | 183 | def focus(window): 184 | '''Focuses the given window.''' 185 | return i3.focus(id=window) 186 | 187 | 188 | def goto_workspace(name): 189 | '''Jump to the given workspace.''' 190 | return i3.workspace(name) 191 | 192 | 193 | def get_current_workspace(): 194 | '''Get the name of the currently active workspace.''' 195 | filtered = [ws for ws in i3.get_workspaces() if ws["focused"] is True] 196 | return filtered[0]['name'] if filtered else None 197 | 198 | 199 | def cycle_numbered_workspaces(backwards=False): 200 | '''Get the next (previous) numbered workspace.''' 201 | current = get_current_workspace() 202 | if not current.isdecimal(): 203 | return None 204 | i = int(current) 205 | return str(i + 1) if not backwards else str(i - 1) 206 | 207 | 208 | def main(): 209 | parser = argparse.ArgumentParser(description='''quickswitch for i3''') 210 | parser.add_argument('-m', '--move', default=False, action="store_true", 211 | help="move window to the current workspace") 212 | 213 | mutgrp = parser.add_mutually_exclusive_group() 214 | mutgrp.add_argument('-s', '--scratchpad', default=False, action="store_true", 215 | help="list scratchpad windows instead of regular ones") 216 | mutgrp.add_argument('-w', '--workspaces', default=False, 217 | action="store_true", 218 | help="list workspaces instead of windows") 219 | mutgrp.add_argument('-e', '--empty', default=False, action='store_true', 220 | help='go to the next empty, numbered workspace') 221 | mutgrp.add_argument('-r', '--regex', 222 | help='find the first window matching the regex and focus/move it') 223 | mutgrp.add_argument('-g', '--degap', action='store_true', 224 | help='make numbered workspaces consecutive (remove gaps)') 225 | mutgrp.add_argument('-n', '--next', default=False, action='store_true', 226 | help='go to the next (numbered) workspace') 227 | mutgrp.add_argument('-p', '--previous', default=False, action='store_true', 228 | help='go to the previous (numbered) workspace') 229 | 230 | parser.add_argument('-d', '--dmenu', default='dmenu -b -i -l 20', help='dmenu command, executed within a shell') 231 | 232 | args = parser.parse_args() 233 | 234 | if not check_dmenu(): 235 | print("quickswitch requires dmenu.") 236 | print("Please install it using your distribution's package manager.") 237 | exit(1) 238 | 239 | # jumping to the next empty workspaces doesn't require going through all 240 | # the stuff below, as we don't need to call dmenu etc, so we just call it 241 | # here and exit if the appropriate flag was given. 242 | if args.empty: 243 | exit(*goto_workspace(next_empty())) 244 | 245 | # likewise for degapping... 246 | if args.degap: 247 | degap() 248 | exit(0) 249 | 250 | # ...and regex search... 251 | if args.regex: 252 | exit(0 if find_window_by_regex(args.regex, args.move) else 1) 253 | 254 | # ...as well as workspace cycling 255 | if args.next or args.previous: 256 | if not get_current_workspace().isdecimal: 257 | print("--next and --previous only work on numbered workspaces") 258 | exit(1) 259 | target_ws = cycle_numbered_workspaces(args.previous) 260 | if not args.move: 261 | exit(*goto_workspace(target_ws)) 262 | else: 263 | exit(*i3.command("move container to workspace {}".format(target_ws))) 264 | 265 | 266 | lookup_func = get_windows 267 | if args.scratchpad: 268 | lookup_func = get_scratchpad 269 | if args.workspaces: 270 | lookup_func = get_workspaces 271 | 272 | action_func = focus 273 | if args.move: 274 | action_func = move_window_here 275 | else: 276 | if args.scratchpad: 277 | action_func = get_scratchpad_window 278 | if args.workspaces: 279 | action_func = goto_workspace 280 | 281 | lookup = lookup_func() 282 | target = dmenu(lookup.keys(), args.dmenu) 283 | id_ = lookup.get(target) 284 | success = action_func(lookup.get(target)) if id_ is not None else False 285 | 286 | exit(0 if success else 1) 287 | 288 | 289 | if __name__ == '__main__': 290 | main() 291 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # quickswitch for i3 - quickly change to and locate windows in i3. 2 | # 3 | # Author: slowpoke 4 | # 5 | # This program is Free Software under the terms of the 6 | # 7 | # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 8 | # Version 2, December 2004 9 | # 10 | # Copyright (C) 2004 Sam Hocevar 11 | # 12 | # Everyone is permitted to copy and distribute verbatim or modified 13 | # copies of this license document, and changing it is allowed as long 14 | # as the name is changed. 15 | # 16 | # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 17 | # TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 18 | # 19 | # 0. You just DO WHAT THE FUCK YOU WANT TO. 20 | 21 | from distutils.core import setup 22 | 23 | import quickswitch 24 | 25 | setup(name='quickswitch-i3', 26 | description='Quickly change to and locate windows in i3', 27 | long_description=open('README.rst').read(), 28 | version=quickswitch.__version__, 29 | author='slowpoke', 30 | author_email='mail+python at slowpoke dot io', 31 | url='https://github.com/proxypoke/quickswitch-for-i3', 32 | scripts=['quickswitch.py'], 33 | requires=['i3_py'], 34 | classifiers=['Intended Audience :: End Users/Desktop', 35 | 'Programming Language :: Python :: 2', 36 | 'Programming Language :: Python :: 3'], 37 | license='DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE') 38 | --------------------------------------------------------------------------------