├── MANIFEST.in ├── README.md ├── examples ├── README.md ├── cycle.py ├── fibonacci.py ├── ipc.py ├── scratcher.py ├── winmenu.py └── wsbar.py ├── i3.py ├── setup.py └── test.py /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include i3.py README.md 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | What is i3? 2 | ----------- 3 | 4 | Well, from [i3's website](http://i3wm.org/) itself: 5 | 6 | > i3 is a tiling window manager, completely written from scratch. The target 7 | > platforms are GNU/Linux and BSD operating systems, our code is Free and Open 8 | > Source Software (FOSS) under the BSD license. i3 is primarily targeted at 9 | > advanced users and developers. 10 | 11 | i3-py contains tools for i3 users and Python developers. To avoid the confusion, 12 | I'll be refering to i3 as _i3-wm_ from here on. 13 | 14 | 15 | Install 16 | ------- 17 | 18 | pip install i3-py 19 | # OR/AND (for Python 2.x) 20 | pip2 install i3-py 21 | 22 | 23 | Run examples 24 | ------------ 25 | 26 | See `examples/` and their 27 | [README](https://github.com/ziberna/i3-py/tree/master/examples). 28 | 29 | 30 | -------------------------------------------------------------------------------- 31 | 32 | Usage 33 | ===== 34 | 35 | The basics 36 | ---------- 37 | 38 | The communication with i3-wm is through sockets. There are 7 types of messages: 39 | 40 | - command (0) 41 | - get_workspaces (1) 42 | - subscribe (2) 43 | - get_outputs (3) 44 | - get_tree (4) 45 | - get_marks (5) 46 | - get_bar_config (6) 47 | 48 | You can control i3-wm with _command_ messages. Other message types return 49 | information about i3-wm without changing its behaviour. 50 | 51 | _Subscribe_ offers 2 event types (read: _changes_) to subscribe for: 52 | 53 | - workspace (0) 54 | - output (1) 55 | 56 | There are various ways to do this with i3.py. Let's start with... 57 | 58 | 59 | Sending commands 60 | ---------------- 61 | 62 | This is best explained by an example. Say you want to switch a layout from the 63 | current one to tabbed. Here's how to do it: 64 | 65 | ```python 66 | import i3 67 | success = i3.layout('tabbed') 68 | if success: 69 | print('Successfully changed layout of the current workspace.') 70 | ``` 71 | 72 | Each command is just a function which accepts any number of parameters. i3.py 73 | formats this function and its parameters into a message to i3-wm. 74 | 75 | None of these functions are actually implemented. i3.py checks each attribute 76 | as it is accessed. If it exists in the module, it returns that attribute. 77 | Otherwise it creates a function on the fly. Calling that function sends a 78 | message to i3-wm, based on the name of the attribute and its parameters. 79 | 80 | ### container criteria 81 | 82 | These dynamic functions also take keyword arguments which specify container 83 | criteria. So if you want to focus a particular window, you can do it like so: 84 | 85 | ```python 86 | i3.focus(title="window title") 87 | ``` 88 | 89 | 90 | Other message types 91 | ------------------- 92 | 93 | OK, _command_ is one type of message, but what about the other ones? Well, they 94 | have to be accessed in a bit different way. You see, when we changed the layout 95 | to tabbed, we didn't have to say that it's a _command_ type of message. But for 96 | other types we'll have to specify the name of the type. 97 | 98 | So, getting a list of workspaces (and displaying them) is as simple as this: 99 | 100 | ```python 101 | import i3 102 | workspaces = i3.get_workspaces() 103 | for workspace in workspaces: 104 | print(workspace['name']) 105 | ``` 106 | 107 | If the attribute you accessed is an existing message type, then the resulting 108 | function sends a message as a parameter. In fact, we could change the current 109 | layout to stacked like so: 110 | 111 | ```python 112 | import i3 113 | i3.command('layout', 'stacking') 114 | ``` 115 | 116 | This works for all message types. Actually, if you want to get even lower, 117 | there's this function: 118 | 119 | ```python 120 | import i3 121 | i3.msg(, ) 122 | ``` 123 | 124 | A message type can be in other formats, as an example here are the alternatives 125 | for get_outputs: GET_OUTPUTS, '3', 3 126 | 127 | i3.py is case insensitive when it comes to message types. This also holds true 128 | for accessing non-existent attributes, like `i3.GeT_OuTpUtS()`. 129 | 130 | 131 | Convenience functions 132 | --------------------- 133 | 134 | Since all returned data is in a form of a dictionary or list, some 135 | convenience function have been written to effectively work with the data. 136 | 137 | ### i3.container 138 | 139 | i3.container will take keyword arguments and formats them into i3-wm's syntax 140 | for container criteria. The resulting string can be used in i3.msg. Example: 141 | 142 | ```python 143 | i3.container(title="abc", con_id=123) # returns '[title="abc" con_id="123"]' 144 | ``` 145 | 146 | This function is also used internally for dynamic methods. 147 | 148 | 149 | ### i3.filter 150 | 151 | Some calls to i3 will return a huge amount of data, namely `i3.get_tree`. It can 152 | be quite stressful to find what you want in such large dictionary. i3-py 153 | provides this convenience function that will filter the given tree: 154 | 155 | ```python 156 | i3.filter(focused=False) 157 | ``` 158 | 159 | The above would get you all unfocused nodes in the tree. One useful thing would 160 | be to get a list of focused windows. Since windows are just leaf nodes (that is, 161 | nodes without sub-nodes), you can do this: 162 | 163 | ```python 164 | i3.filter(nodes=[], focused=True) 165 | ``` 166 | 167 | You can also supply your own tree with `tree` keyword argument. 168 | 169 | 170 | Lets continue to more advanced stuff... 171 | 172 | 173 | Subscribing to events 174 | --------------------- 175 | 176 | Say you want to display information about workspaces whenever a new workspaces 177 | is created. There's a function for that called _subscribe_: 178 | 179 | ```python 180 | import i3 181 | i3.subscribe('workspace') 182 | ``` 183 | 184 | _Workspace_ is one of the two event types. The other one is _output_, which 185 | watches for output changes. 186 | 187 | Just displaying the list of workspaces isn't very useful, we want to actually 188 | do something. Because of that, you can define your own subscription: 189 | 190 | ```python 191 | import i3 192 | 193 | def my_function(event, data, subscription): 194 | 195 | if : 196 | subscription.close() 197 | 198 | subscription = i3.Subscription(my_function, 'workspace') 199 | ``` 200 | 201 | There are more parameters available for Subscription class, but some are too 202 | advanced for what has been explained so far. 203 | 204 | -------------------------------------------------------------------------------- 205 | __NOTE:__ Everything in i3-py project contains a doc string. You can get help 206 | about any feature like so: 207 | 208 | ```python 209 | import i3 210 | help(i3.Subscription) 211 | ``` 212 | 213 | -------------------------------------------------------------------------------- 214 | 215 | Okay, so now let's move to some of the more lower-level stuff... 216 | 217 | 218 | Sockets 219 | ------- 220 | 221 | Sockets are created with the help of `i3.Socket` class. The class has the 222 | following parameters, all of them optional: 223 | 224 | - path of the i3-wm's socket 225 | - timeout in seconds when receiving the message 226 | - chunk size in bytes of a single chunk that is send to i3-wm 227 | - magic string, that i3-wm checks for (it is "i3-ipc") 228 | 229 | The path, if not provided, is retrieved via this unmentioned function: 230 | 231 | ```python 232 | i3.get_socket_path() 233 | ``` 234 | 235 | The most common-stuff methods of an `i3.Socket` object are `connect`, `close` 236 | and `msg(msg_type, payload='')`. Example of usage: 237 | 238 | ```python 239 | import i3 240 | socket = i3.Socket() 241 | response = socket.msg(`command`, `focus right`) 242 | socket.close() 243 | ``` 244 | 245 | To check if socket has been closed use the `socket.connected` property. There's 246 | even more lower-level stuff, like packing and unpacking the payload, sending 247 | it and receiving it... See the docs for these. 248 | 249 | 250 | Exceptions 251 | ---------- 252 | 253 | There are three exceptions: 254 | 255 | - `i3.MessageTypeError`, raised when you use unavailable message type 256 | - `i3.EventTypeError`, raised when you use unavaible event type 257 | - `i3.MessageError`, raised when i3 sends back an error (the exception contains 258 | that error string) 259 | 260 | If you want to get the list of available ones from Python, use `i3.MSG_TYPES` 261 | and `i3.EVENT_TYPES`. 262 | 263 | Okay, that's all for now. Some stuff has been left out, so be 264 | sure to check the docs via Python's `help` function. 265 | 266 | 267 | -------------------------------------------------------------------------------- 268 | 269 | About 270 | ===== 271 | 272 | Author: Jure Žiberna 273 | License: GNU GPL 3 274 | 275 | Thanks: 276 | 277 | - [i3 window manager](http://i3wm.org/) and its author Michael Stapelberg 278 | - [Nathan Middleton and his i3ipc](http://github.com/thepub/i3ipc) and its 279 | current maintainer [David Bronke](http://github.com/whitelynx/i3ipc). The 280 | existing project was used as a reference on how to implement sockets in 281 | Python. i3-py fixed some of the critical bugs that i3ipc contains and 282 | added more high-level features in addition to lower-level ones. 283 | 284 | References: 285 | 286 | - [i3-wm's ipc page](http://i3wm.org/docs/ipc.html) has more information 287 | about i3-ipc interface. 288 | - [i3-wm's user guide](http://i3wm.org/docs/userguide.html) contains lots of 289 | commands that you can use with i3-py. 290 | 291 | i3-py was tested with Python 3.2.2 and 2.7.2. 292 | 293 | Dependencies: 294 | 295 | - i3-wm 296 | - Python 297 | 298 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | i3 usage examples 2 | ================= 3 | 4 | cycle.py 5 | -------- 6 | 7 | __cycle.py__ will cycle through all windows (by focusing them) and stop at the 8 | previously focused one(s). This is done by filtering currently focused and 9 | unfocused windows with the help of `i3.filter` function, and the focusing each 10 | of them with the help of `i3.window` function. 11 | 12 | 13 | fibonaccy.py 14 | ------------ 15 | 16 | __fibonacci.py__ will switch to a (new) workspace named 'fibonacci', then open 17 | default terminal windows in a fibonacci spiral. All newly opened terminals will 18 | close after a few seconds and the script will switch back to the previously 19 | focused workspace. 20 | 21 | 22 | ipc.py 23 | ------ 24 | 25 | __ipc.py__ is a close clone of the i3-msg; it is a command-line tool for sending 26 | messages to i3-wm. 27 | 28 | usage: i3-ipc [-h] [-s ] [-t ] [-T ] 29 | [ [ ...]] 30 | 31 | i3-ipc 0.5.5 (2012-03-29). Implemented in Python. 32 | 33 | positional arguments: 34 | message or "payload" to send, can be multiple strings 35 | 36 | optional arguments: 37 | -h, --help show this help message and exit 38 | -s custom path to an i3 socket file 39 | -t message type in text form (e.g. "get_tree") 40 | -T seconds before socket times out, floating point values allowed 41 | 42 | wsbar.py 43 | -------- 44 | 45 | __wsbar.py__ launches a __dzen2__ bar that displays current workspaces. 46 | 47 | 48 | winmenu.py 49 | ---------- 50 | 51 | __winmenu.py__ launches dmenu (with vertical patch) with a list of clients, 52 | sorted after workspaces. Selecting a client jumps to that window. 53 | 54 | -------------------------------------------------------------------------------- /examples/cycle.py: -------------------------------------------------------------------------------- 1 | import i3 2 | import time 3 | 4 | def cycle(): 5 | # get currently focused windows 6 | current = i3.filter(nodes=[], focused=True) 7 | # get unfocused windows 8 | other = i3.filter(nodes=[], focused=False) 9 | # focus each previously unfocused window for 0.5 seconds 10 | for window in other: 11 | i3.focus(con_id=window['id']) 12 | time.sleep(0.5) 13 | # focus the original windows 14 | for window in current: 15 | i3.focus(con_id=window['id']) 16 | 17 | if __name__ == '__main__': 18 | cycle() 19 | -------------------------------------------------------------------------------- /examples/fibonacci.py: -------------------------------------------------------------------------------- 1 | import i3 2 | import os 3 | import time 4 | 5 | term = os.environ.get('TERM', 'xterm') 6 | if 'rxvt-unicode' in term: 7 | term = 'urxvt' 8 | 9 | def fibonacci(num): 10 | i3.exec(term) 11 | time.sleep(0.5) 12 | if num % 2 == 0: 13 | if num % 4 == 0: 14 | i3.focus('up') 15 | i3.split('h') 16 | else: 17 | if num % 4 == 1: 18 | i3.focus('left') 19 | i3.split('v') 20 | if num > 1: 21 | fibonacci(num - 1) 22 | 23 | def run(num): 24 | # current workspace 25 | current = [ws for ws in i3.get_workspaces() if ws['focused']][0] 26 | # switch to workspace named 'fibonacci' 27 | i3.workspace('fibonacci') 28 | i3.layout('default') 29 | fibonacci(num) 30 | time.sleep(3) 31 | # close all opened terminals 32 | for n in range(num): 33 | i3.kill() 34 | time.sleep(0.5) 35 | i3.workspace(current['name']) 36 | 37 | if __name__ == '__main__': 38 | run(8) 39 | -------------------------------------------------------------------------------- /examples/ipc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #====================================================================== 3 | # i3 (Python module for communicating with i3 window manager) 4 | # Copyright (C) 2012 Jure Ziberna 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | #====================================================================== 19 | 20 | 21 | import os 22 | import argparse 23 | 24 | import i3 25 | 26 | 27 | # Generate description based on current version and its date 28 | DESCRIPTION = 'i3-ipc %s (%s).' % (i3.__version__, i3.__date__) 29 | DESCRIPTION += ' Implemented in Python.' 30 | 31 | # Dictionary of command-line help messages 32 | HELP = { 33 | 'socket': "custom path to an i3 socket file", 34 | 'type': "message type in text form (e.g. \"get_tree\")", 35 | 'timeout': "seconds before socket times out, floating point values allowed", 36 | 'message': "message or \"payload\" to send, can be multiple strings", 37 | } 38 | 39 | 40 | def parse(): 41 | """ 42 | Creates argument parser for parsing command-line arguments. Returns parsed 43 | arguments in a form of a namespace. 44 | """ 45 | # Setting up argument parses 46 | parser = argparse.ArgumentParser(description=DESCRIPTION) 47 | parser.add_argument('-s', metavar='', dest='socket', type=str, default=None, help=HELP['socket']) 48 | parser.add_argument('-t', metavar='', dest='type', type=str, default='command', help=HELP['type']) 49 | parser.add_argument('-T', metavar='', dest='timeout', type=float, default=None, help=HELP['timeout']) 50 | parser.add_argument('', type=str, nargs='*', help=HELP['message']) 51 | # Parsing and hacks 52 | args = parser.parse_args() 53 | message = args.__dict__[''] 54 | args.message = ' '.join(message) 55 | return args 56 | 57 | 58 | def main(socket, type, timeout, message): 59 | """ 60 | Excepts arguments and evaluates them. 61 | """ 62 | if not socket: 63 | socket = i3.get_socket_path() 64 | if not socket: 65 | print("Couldn't get socket path. Are you sure i3 is running?") 66 | return False 67 | # Initializes default socket with given path and timeout 68 | try: 69 | i3.default_socket(i3.Socket(path=socket, timeout=timeout)) 70 | except i3.ConnectionError: 71 | print("Couldn't connect to socket at '%s'." % socket) 72 | return False 73 | # Format input 74 | if type in i3.EVENT_TYPES: 75 | event_type = type 76 | event = message 77 | type = 'subscribe' 78 | elif type == 'subscribe': 79 | message = message.split(' ') 80 | message_len = len(message) 81 | if message_len >= 1: 82 | event_type = message[0] 83 | if message_len >= 2: 84 | event = ' '.join(message[1:]) 85 | else: 86 | event = '' 87 | else: 88 | # Let if fail 89 | event_type = '' 90 | try: 91 | if type == 'subscribe': 92 | i3.subscribe(event_type, event) 93 | else: 94 | output = i3.msg(type, message) 95 | print(output) 96 | except i3.i3Exception as i3error: 97 | print(i3error) 98 | 99 | 100 | if __name__ == '__main__': 101 | args = parse() 102 | main(args.socket, args.type, args.timeout, args.message) 103 | 104 | -------------------------------------------------------------------------------- /examples/scratcher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Cycling through scratchpad windows... 4 | 5 | Add this to your i3 config file: 6 | bindsym exec python /path/to/this/script.py 7 | """ 8 | 9 | import i3 10 | 11 | def scratchpad_windows(): 12 | # get containers with appropriate scratchpad state 13 | containers = i3.filter(scratchpad_state='changed') 14 | # filter out windows (leaf nodes of the above containers) 15 | return i3.filter(containers, nodes=[]) 16 | 17 | def main(): 18 | windows = scratchpad_windows() 19 | # search for focused window among scratchpad windows 20 | if i3.filter(windows, focused=True): 21 | # move that window back to scratchpad 22 | i3.move('scratchpad') 23 | # show the next scratchpad window 24 | i3.scratchpad('show') 25 | 26 | if __name__ == '__main__': 27 | main() 28 | 29 | -------------------------------------------------------------------------------- /examples/winmenu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # dmenu script to jump to windows in i3. 3 | # 4 | # using ziberna's i3-py library: https://github.com/ziberna/i3-py 5 | # depends: dmenu (vertical patch), i3. 6 | # released by joepd under WTFPLv2-license: 7 | # http://sam.zoy.org/wtfpl/COPYING 8 | # 9 | # edited by Jure Ziberna for i3-py's examples section 10 | 11 | import i3 12 | import subprocess 13 | 14 | def i3clients(): 15 | """ 16 | Returns a dictionary of key-value pairs of a window text and window id. 17 | Each window text is of format "[workspace] window title (instance number)" 18 | """ 19 | clients = {} 20 | for ws_num in range(1,11): 21 | workspace = i3.filter(num=ws_num) 22 | if not workspace: 23 | continue 24 | workspace = workspace[0] 25 | windows = i3.filter(workspace, nodes=[]) 26 | instances = {} 27 | # Adds windows and their ids to the clients dictionary 28 | for window in windows: 29 | win_str = '[%s] %s' % (workspace['name'], window['name']) 30 | # Appends an instance number if other instances are present 31 | if win_str in instances: 32 | instances[win_str] += 1 33 | win_str = '%s (%d)' % (win_str, instances[win_str]) 34 | else: 35 | instances[win_str] = 1 36 | clients[win_str] = window['id'] 37 | return clients 38 | 39 | def win_menu(clients, l=10): 40 | """ 41 | Displays a window menu using dmenu. Returns window id. 42 | """ 43 | dmenu = subprocess.Popen(['/usr/bin/dmenu','-i','-l', str(l)], 44 | stdin=subprocess.PIPE, 45 | stdout=subprocess.PIPE) 46 | menu_str = '\n'.join(sorted(clients.keys())) 47 | # Popen.communicate returns a tuple stdout, stderr 48 | win_str = dmenu.communicate(menu_str.encode('utf-8'))[0].decode('utf-8').rstrip() 49 | return clients.get(win_str, None) 50 | 51 | if __name__ == '__main__': 52 | clients = i3clients() 53 | win_id = win_menu(clients) 54 | if win_id: 55 | i3.focus(con_id=win_id) 56 | 57 | -------------------------------------------------------------------------------- /examples/wsbar.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #====================================================================== 3 | # i3 (Python module for communicating with i3 window manager) 4 | # Copyright (C) 2012 Jure Ziberna 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | #====================================================================== 19 | 20 | 21 | import sys 22 | import time 23 | import subprocess 24 | 25 | import i3 26 | 27 | 28 | class Colors(object): 29 | """ 30 | A class for easier managing of bar's colors. 31 | Attributes (hexadecimal color values): 32 | - background 33 | - statusline (foreground) 34 | (below are (foreground, background) tuples for workspace buttons) 35 | - focused 36 | - active (when a workspace is opened on unfocused output) 37 | - inactive (unfocused workspace) 38 | - urgent 39 | The naming comes from i3-wm itself. 40 | Default values are also i3-wm's defaults. 41 | """ 42 | # bar colors 43 | background = '#000000' 44 | statusline = '#ffffff' 45 | # workspace button colors 46 | focused = ('#ffffff', '#285577') 47 | active = ('#ffffff', '#333333') 48 | inactive = ('#888888', '#222222') 49 | urgent = ('#ffffff', '#900000') 50 | 51 | def get_color(self, workspace, output): 52 | """ 53 | Returns a (foreground, background) tuple based on given workspace 54 | state. 55 | """ 56 | if workspace['focused']: 57 | if output['current_workspace'] == workspace['name']: 58 | return self.focused 59 | else: 60 | return self.active 61 | if workspace['urgent']: 62 | return self.urgent 63 | else: 64 | return self.inactive 65 | 66 | 67 | class i3wsbar(object): 68 | """ 69 | A workspace bar; display a list of workspaces using a given bar 70 | application. Defaults to dzen2. 71 | Changeable settings (attributes): 72 | - button_format 73 | - bar_format 74 | - colors (see i3wsbar.Colors docs) 75 | - font 76 | - bar_command (the bar application) 77 | - bar_arguments (command-line arguments for the bar application) 78 | """ 79 | # bar formatting (set for dzen) 80 | button_format = '^bg(%s)^ca(1,i3-ipc workspace %s)^fg(%s)%s^ca()^bg() ' 81 | bar_format = '^p(_LEFT) %s^p(_RIGHT) ' 82 | # default bar style 83 | colors = Colors() 84 | font = '-misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1' 85 | # default bar settings 86 | bar_command = 'dzen2' 87 | bar_arguments = ['-dock', '-fn', font, '-bg', colors.background, '-fg', colors.statusline] 88 | 89 | def __init__(self, colors=None, font=None, bar_cmd=None, bar_args=None): 90 | if colors: 91 | self.colors = colors 92 | if font: 93 | self.font = font 94 | if bar_cmd: 95 | self.dzen_command = bar_cmd 96 | if bar_args: 97 | self.bar_arguments = bar_args 98 | # Initialize bar application... 99 | args = [self.bar_command] + self.bar_arguments 100 | self.bar = subprocess.Popen(args, stdin=subprocess.PIPE) 101 | # ...and socket 102 | self.socket = i3.Socket() 103 | # Output to the bar right away 104 | workspaces = self.socket.get('get_workspaces') 105 | outputs = self.socket.get('get_outputs') 106 | self.display(self.format(workspaces, outputs)) 107 | # Subscribe to an event 108 | callback = lambda data, event, _: self.change(data, event) 109 | self.subscription = i3.Subscription(callback, 'workspace') 110 | 111 | def change(self, event, workspaces): 112 | """ 113 | Receives event and workspace data, changes the bar if change is 114 | present in event. 115 | """ 116 | if 'change' in event: 117 | outputs = self.socket.get('get_outputs') 118 | bar_text = self.format(workspaces, outputs) 119 | self.display(bar_text) 120 | 121 | def format(self, workspaces, outputs): 122 | """ 123 | Formats the bar text according to the workspace data given. 124 | """ 125 | bar = '' 126 | for workspace in workspaces: 127 | output = None 128 | for output_ in outputs: 129 | if output_['name'] == workspace['output']: 130 | output = output_ 131 | break 132 | if not output: 133 | continue 134 | foreground, background = self.colors.get_color(workspace, output) 135 | if not foreground: 136 | continue 137 | name = workspace['name'] 138 | button = self.button_format % (background, "'"+name+"'", foreground, name) 139 | bar += button 140 | return self.bar_format % bar 141 | 142 | def display(self, bar_text): 143 | """ 144 | Displays a text on the bar by piping it to the bar application. 145 | """ 146 | bar_text += '\n' 147 | try: 148 | bar_text = bar_text.encode() 149 | except AttributeError: 150 | pass # already a byte string 151 | self.bar.stdin.write(bar_text) 152 | 153 | def quit(self): 154 | """ 155 | Quits the i3wsbar; closes the subscription and terminates the bar 156 | application. 157 | """ 158 | self.subscription.close() 159 | self.bar.terminate() 160 | 161 | 162 | if __name__ == '__main__': 163 | args = sys.argv[1:] 164 | bar = i3wsbar(bar_args=args) 165 | try: 166 | while True: 167 | time.sleep(1) 168 | except KeyboardInterrupt: 169 | print('') # force new line 170 | finally: 171 | bar.quit() 172 | 173 | -------------------------------------------------------------------------------- /i3.py: -------------------------------------------------------------------------------- 1 | #====================================================================== 2 | # i3 (Python module for communicating with i3 window manager) 3 | # Copyright (C) 2012 Jure Ziberna 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | #====================================================================== 18 | 19 | 20 | import sys 21 | import subprocess 22 | import json 23 | import socket 24 | import struct 25 | import threading 26 | import time 27 | 28 | ModuleType = type(sys) 29 | 30 | 31 | __author__ = 'Jure Ziberna' 32 | __version__ = '0.6.5' 33 | __date__ = '2012-06-20' 34 | __license__ = 'GNU GPL 3' 35 | 36 | 37 | MSG_TYPES = [ 38 | 'command', 39 | 'get_workspaces', 40 | 'subscribe', 41 | 'get_outputs', 42 | 'get_tree', 43 | 'get_marks', 44 | 'get_bar_config', 45 | ] 46 | 47 | EVENT_TYPES = [ 48 | 'workspace', 49 | 'output', 50 | ] 51 | 52 | 53 | class i3Exception(Exception): 54 | pass 55 | 56 | class MessageTypeError(i3Exception): 57 | """ 58 | Raised when message type isn't available. See i3.MSG_TYPES. 59 | """ 60 | def __init__(self, type): 61 | msg = "Message type '%s' isn't available" % type 62 | super(MessageTypeError, self).__init__(msg) 63 | 64 | class EventTypeError(i3Exception): 65 | """ 66 | Raised when even type isn't available. See i3.EVENT_TYPES. 67 | """ 68 | def __init__(self, type): 69 | msg = "Event type '%s' isn't available" % type 70 | super(EventTypeError, self).__init__(msg) 71 | 72 | class MessageError(i3Exception): 73 | """ 74 | Raised when a message to i3 is unsuccessful. 75 | That is, when it contains 'success': false in its JSON formatted response. 76 | """ 77 | pass 78 | 79 | class ConnectionError(i3Exception): 80 | """ 81 | Raised when a socket couldn't connect to the window manager. 82 | """ 83 | def __init__(self, socket_path): 84 | msg = "Could not connect to socket at '%s'" % socket_path 85 | super(ConnectionError, self).__init__(msg) 86 | 87 | 88 | def parse_msg_type(msg_type): 89 | """ 90 | Returns an i3-ipc code of the message type. Raises an exception if 91 | the given message type isn't available. 92 | """ 93 | try: 94 | index = int(msg_type) 95 | except ValueError: 96 | index = -1 97 | if index >= 0 and index < len(MSG_TYPES): 98 | return index 99 | msg_type = str(msg_type).lower() 100 | if msg_type in MSG_TYPES: 101 | return MSG_TYPES.index(msg_type) 102 | else: 103 | raise MessageTypeError(msg_type) 104 | 105 | def parse_event_type(event_type): 106 | """ 107 | Returns an i3-ipc string of the event_type. Raises an exception if 108 | the given event type isn't available. 109 | """ 110 | try: 111 | index = int(event_type) 112 | except ValueError: 113 | index = -1 114 | if index >= 0 and index < len(EVENT_TYPES): 115 | return EVENT_TYPES[index] 116 | event_type = str(event_type).lower() 117 | if event_type in EVENT_TYPES: 118 | return event_type 119 | else: 120 | raise EventTypeError(event_type) 121 | 122 | 123 | class Socket(object): 124 | """ 125 | Socket for communicating with the i3 window manager. 126 | Optional arguments: 127 | - path of the i3 socket. Path is retrieved from i3-wm itself via 128 | "i3.get_socket_path()" if not provided. 129 | - timeout in seconds 130 | - chunk_size in bytes 131 | - magic_string as a safety string for i3-ipc. Set to 'i3-ipc' by default. 132 | """ 133 | magic_string = 'i3-ipc' # safety string for i3-ipc 134 | chunk_size = 1024 # in bytes 135 | timeout = 0.5 # in seconds 136 | buffer = b'' # byte string 137 | 138 | def __init__(self, path=None, timeout=None, chunk_size=None, 139 | magic_string=None): 140 | if not path: 141 | path = get_socket_path() 142 | self.path = path 143 | if timeout: 144 | self.timeout = timeout 145 | if chunk_size: 146 | self.chunk_size = chunk_size 147 | if magic_string: 148 | self.magic_string = magic_string 149 | # Socket initialization and connection 150 | self.initialize() 151 | self.connect() 152 | # Struct format initialization, length of magic string is in bytes 153 | self.struct_header = '<%dsII' % len(self.magic_string.encode('utf-8')) 154 | self.struct_header_size = struct.calcsize(self.struct_header) 155 | 156 | def initialize(self): 157 | """ 158 | Initializes the socket. 159 | """ 160 | self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 161 | self.socket.settimeout(self.timeout) 162 | 163 | def connect(self, path=None): 164 | """ 165 | Connects the socket to socket path if not already connected. 166 | """ 167 | if not self.connected: 168 | self.initialize() 169 | if not path: 170 | path = self.path 171 | try: 172 | self.socket.connect(path) 173 | except socket.error: 174 | raise ConnectionError(path) 175 | 176 | def get(self, msg_type, payload=''): 177 | """ 178 | Convenience method, calls "socket.send(msg_type, payload)" and 179 | returns data from "socket.receive()". 180 | """ 181 | self.send(msg_type, payload) 182 | return self.receive() 183 | 184 | def subscribe(self, event_type, event=None): 185 | """ 186 | Subscribes to an event. Returns data on first occurrence. 187 | """ 188 | event_type = parse_event_type(event_type) 189 | # Create JSON payload from given event type and event 190 | payload = [event_type] 191 | if event: 192 | payload.append(event) 193 | payload = json.dumps(payload) 194 | return self.get('subscribe', payload) 195 | 196 | def send(self, msg_type, payload=''): 197 | """ 198 | Sends the given message type with given message by packing them 199 | and continuously sending bytes from the packed message. 200 | """ 201 | message = self.pack(msg_type, payload) 202 | # Continuously send the bytes from the message 203 | self.socket.sendall(message) 204 | 205 | def receive(self): 206 | """ 207 | Tries to receive a data. Unpacks the received byte string if 208 | successful. Returns None on failure. 209 | """ 210 | try: 211 | data = self.socket.recv(self.chunk_size) 212 | msg_magic, msg_length, msg_type = self.unpack_header(data) 213 | msg_size = self.struct_header_size + msg_length 214 | # Keep receiving data until the whole message gets through 215 | while len(data) < msg_size: 216 | data += self.socket.recv(msg_length) 217 | data = self.buffer + data 218 | return self.unpack(data) 219 | except socket.timeout: 220 | return None 221 | 222 | def pack(self, msg_type, payload): 223 | """ 224 | Packs the given message type and payload. Turns the resulting 225 | message into a byte string. 226 | """ 227 | msg_magic = self.magic_string 228 | # Get the byte count instead of number of characters 229 | msg_length = len(payload.encode('utf-8')) 230 | msg_type = parse_msg_type(msg_type) 231 | # "struct.pack" returns byte string, decoding it for concatenation 232 | msg_length = struct.pack('I', msg_length).decode('utf-8') 233 | msg_type = struct.pack('I', msg_type).decode('utf-8') 234 | message = '%s%s%s%s' % (msg_magic, msg_length, msg_type, payload) 235 | # Encoding the message back to byte string 236 | return message.encode('utf-8') 237 | 238 | def unpack(self, data): 239 | """ 240 | Unpacks the given byte string and parses the result from JSON. 241 | Returns None on failure and saves data into "self.buffer". 242 | """ 243 | data_size = len(data) 244 | msg_magic, msg_length, msg_type = self.unpack_header(data) 245 | msg_size = self.struct_header_size + msg_length 246 | # Message shouldn't be any longer than the data 247 | if data_size >= msg_size: 248 | payload = data[self.struct_header_size:msg_size].decode('utf-8') 249 | payload = json.loads(payload) 250 | self.buffer = data[msg_size:] 251 | return payload 252 | else: 253 | self.buffer = data 254 | return None 255 | 256 | def unpack_header(self, data): 257 | """ 258 | Unpacks the header of given byte string. 259 | """ 260 | return struct.unpack(self.struct_header, data[:self.struct_header_size]) 261 | 262 | @property 263 | def connected(self): 264 | """ 265 | Returns True if connected and False if not. 266 | """ 267 | try: 268 | self.get('command') 269 | return True 270 | except socket.error: 271 | return False 272 | 273 | def close(self): 274 | """ 275 | Closes the socket connection. 276 | """ 277 | self.socket.close() 278 | 279 | 280 | class Subscription(threading.Thread): 281 | """ 282 | Creates a new subscription and runs a listener loop. Calls the 283 | callback on event. 284 | Example parameters: 285 | callback = lambda event, data, subscription: print(data) 286 | event_type = 'workspace' 287 | event = 'focus' 288 | event_socket = 289 | data_socket = 290 | """ 291 | subscribed = False 292 | type_translation = { 293 | 'workspace': 'get_workspaces', 294 | 'output': 'get_outputs' 295 | } 296 | 297 | def __init__(self, callback, event_type, event=None, event_socket=None, 298 | data_socket=None): 299 | # Variable initialization 300 | if not callable(callback): 301 | raise TypeError('Callback must be callable') 302 | event_type = parse_event_type(event_type) 303 | self.callback = callback 304 | self.event_type = event_type 305 | self.event = event 306 | # Socket initialization 307 | if not event_socket: 308 | event_socket = Socket() 309 | self.event_socket = event_socket 310 | self.event_socket.subscribe(event_type, event) 311 | if not data_socket: 312 | data_socket = Socket() 313 | self.data_socket = data_socket 314 | # Thread initialization 315 | threading.Thread.__init__(self) 316 | self.start() 317 | 318 | def run(self): 319 | """ 320 | Wrapper method for the listen method -- handles exceptions. 321 | The method is run by the underlying "threading.Thread" object. 322 | """ 323 | try: 324 | self.listen() 325 | except socket.error: 326 | self.close() 327 | 328 | def listen(self): 329 | """ 330 | Runs a listener loop until self.subscribed is set to False. 331 | Calls the given callback method with data and the object itself. 332 | If event matches the given one, then matching data is retrieved. 333 | Otherwise, the event itself is sent to the callback. 334 | In that case 'change' key contains the thing that was changed. 335 | """ 336 | self.subscribed = True 337 | while self.subscribed: 338 | event = self.event_socket.receive() 339 | if not event: # skip an iteration if event is None 340 | continue 341 | if not self.event or ('change' in event and event['change'] == self.event): 342 | msg_type = self.type_translation[self.event_type] 343 | data = self.data_socket.get(msg_type) 344 | else: 345 | data = None 346 | self.callback(event, data, self) 347 | self.close() 348 | 349 | def close(self): 350 | """ 351 | Ends subscription loop by setting self.subscribed to False and 352 | closing both sockets. 353 | """ 354 | self.subscribed = False 355 | self.event_socket.close() 356 | if self.data_socket is not default_socket(): 357 | self.data_socket.close() 358 | 359 | 360 | def __call_cmd__(cmd): 361 | """ 362 | Returns output (stdout or stderr) of the given command args. 363 | """ 364 | try: 365 | output = subprocess.check_output(cmd) 366 | except subprocess.CalledProcessError as error: 367 | output = error.output 368 | output = output.decode('utf-8') # byte string decoding 369 | return output.strip() 370 | 371 | 372 | __socket__ = None 373 | def default_socket(socket=None): 374 | """ 375 | Returns i3.Socket object, which was initiliazed once with default values 376 | if no argument is given. 377 | Otherwise sets the default socket to the given socket. 378 | """ 379 | global __socket__ 380 | if socket and isinstance(socket, Socket): 381 | __socket__ = socket 382 | elif not __socket__: 383 | __socket__ = Socket() 384 | return __socket__ 385 | 386 | 387 | def msg(type, message=''): 388 | """ 389 | Takes a message type and a message itself. 390 | Talks to the i3 via socket and returns the response from the socket. 391 | """ 392 | response = default_socket().get(type, message) 393 | return response 394 | 395 | 396 | def __function__(type, message='', *args, **crit): 397 | """ 398 | Accepts a message type, a message. Takes optional args and keyword 399 | args which are present in all future calls of the resulting function. 400 | Returns a function, which takes arguments and container criteria. 401 | If message type was 'command', the function returns success value. 402 | """ 403 | def function(*args2, **crit2): 404 | msg_full = ' '.join([message] + list(args) + list(args2)) 405 | criteria = dict(crit) 406 | criteria.update(crit2) 407 | if criteria: 408 | msg_full = '%s %s' % (container(**criteria), msg_full) 409 | response = msg(type, msg_full) 410 | response = success(response) 411 | if isinstance(response, i3Exception): 412 | raise response 413 | return response 414 | function.__name__ = type 415 | function.__doc__ = 'Message sender (type: %s, message: %s)' % (type, message) 416 | return function 417 | 418 | 419 | def subscribe(event_type, event=None, callback=None): 420 | """ 421 | Accepts an event_type and event itself. 422 | Creates a new subscription, prints data on every event until 423 | KeyboardInterrupt is raised. 424 | """ 425 | if not callback: 426 | def callback(event, data, subscription): 427 | print('changed:', event['change']) 428 | if data: 429 | print('data:\n', data) 430 | 431 | socket = default_socket() 432 | subscription = Subscription(callback, event_type, event, data_socket=socket) 433 | try: 434 | while True: 435 | time.sleep(1) 436 | except KeyboardInterrupt: 437 | print('') # force newline 438 | finally: 439 | subscription.close() 440 | 441 | 442 | def get_socket_path(): 443 | """ 444 | Gets the socket path via i3 command. 445 | """ 446 | cmd = ['i3', '--get-socketpath'] 447 | output = __call_cmd__(cmd) 448 | return output 449 | 450 | 451 | def success(response): 452 | """ 453 | Convenience method for filtering success values of a response. 454 | Each success dictionary is replaces with boolean value. 455 | i3.MessageError is returned if error key is found in any of the 456 | success dictionaries. 457 | """ 458 | if isinstance(response, dict) and 'success' in response: 459 | if 'error' in response: 460 | return MessageError(response['error']) 461 | return response['success'] 462 | elif isinstance(response, list): 463 | for index, item in enumerate(response): 464 | item = success(item) 465 | if isinstance(item, i3Exception): 466 | return item 467 | response[index] = item 468 | return response 469 | 470 | 471 | def container(**criteria): 472 | """ 473 | Turns keyword arguments into a formatted container criteria. 474 | """ 475 | criteria = ['%s="%s"' % (key, val) for key, val in criteria.items()] 476 | return '[%s]' % ' '.join(criteria) 477 | 478 | 479 | def parent(con_id, tree=None): 480 | """ 481 | Searches for a parent of a node/container, given the container id. 482 | Returns None if no container with given id exists (or if the 483 | container is already a root node). 484 | """ 485 | def has_child(node): 486 | for child in node['nodes']: 487 | if child['id'] == con_id: 488 | return True 489 | return False 490 | parents = filter(tree, has_child) 491 | if not parents or len(parents) > 1: 492 | return None 493 | return parents[0] 494 | 495 | 496 | def filter(tree=None, function=None, **conditions): 497 | """ 498 | Filters a tree based on given conditions. For example, to get a list of 499 | unfocused windows (leaf nodes) in the current tree: 500 | i3.filter(nodes=[], focused=False) 501 | The return value is always a list of matched items, even if there's 502 | only one item that matches. 503 | The user function should take a single node. The function doesn't have 504 | to do any dict key or index checking (this is handled by i3.filter 505 | internally). 506 | """ 507 | if tree is None: 508 | tree = msg('get_tree') 509 | elif isinstance(tree, list): 510 | tree = {'list': tree} 511 | if function: 512 | try: 513 | if function(tree): 514 | return [tree] 515 | except (KeyError, IndexError): 516 | pass 517 | else: 518 | for key, value in conditions.items(): 519 | if key not in tree or tree[key] != value: 520 | break 521 | else: 522 | return [tree] 523 | matches = [] 524 | for nodes in ['nodes', 'floating_nodes', 'list']: 525 | if nodes in tree: 526 | for node in tree[nodes]: 527 | matches += filter(node, function, **conditions) 528 | return matches 529 | 530 | 531 | class i3(ModuleType): 532 | """ 533 | i3.py is a Python module for communicating with the i3 window manager. 534 | """ 535 | def __init__(self, module): 536 | self.__module__ = module 537 | self.__name__ = module.__name__ 538 | 539 | def __getattr__(self, name): 540 | """ 541 | Turns a nonexistent attribute into a function. 542 | Returns the resulting function. 543 | """ 544 | try: 545 | return getattr(self.__module__, name) 546 | except AttributeError: 547 | pass 548 | if name.lower() in self.__module__.MSG_TYPES: 549 | return self.__module__.__function__(type=name) 550 | else: 551 | return self.__module__.__function__(type='command', message=name) 552 | 553 | 554 | # Turn the module into an i3 object 555 | sys.modules[__name__] = i3(sys.modules[__name__]) 556 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | long_description = """ 4 | Documentation: https://github.com/ziberna/i3-py/blob/master/README.md 5 | 6 | Examples: https://github.com/ziberna/i3-py/tree/master/examples 7 | 8 | """ 9 | 10 | setup( 11 | name='i3-py', 12 | description='tools for i3 users and developers', 13 | long_description=long_description, 14 | author='Jure Ziberna', 15 | author_email='jure@ziberna.org', 16 | url='https://github.com/ziberna/i3-py', 17 | version='0.6.5', 18 | license='GNU GPL 3', 19 | py_modules=['i3'] 20 | ) 21 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import i3 2 | import unittest 3 | import platform 4 | py3 = platform.python_version_tuple() > ('3',) 5 | 6 | class ParseTest(unittest.TestCase): 7 | def setUp(self): 8 | self.msg_types = ['get_tree', 4, '4'] 9 | self.event_types = ['output', 1, '1'] 10 | 11 | def test_msg_parse(self): 12 | msg_types = [] 13 | for msg_type in self.msg_types: 14 | msg_types.append(i3.parse_msg_type(msg_type)) 15 | for index in range(-1, len(msg_types) - 1): 16 | self.assertEqual(msg_types[index], msg_types[index+1]) 17 | self.assertIsInstance(msg_types[index], int) 18 | 19 | def test_event_parse(self): 20 | event_types = [] 21 | for event_type in self.event_types: 22 | event_types.append(i3.parse_event_type(event_type)) 23 | for index in range(-1, len(event_types) - 1): 24 | self.assertEqual(event_types[index], event_types[index+1]) 25 | self.assertIsInstance(event_types[index], str) 26 | 27 | def test_msg_type_error(self): 28 | border_lower = -1 29 | border_higher = len(i3.MSG_TYPES) 30 | values = ['joke', border_lower, border_higher, -100, 100] 31 | for val in values: 32 | self.assertRaises(i3.MessageTypeError, i3.parse_msg_type, val) 33 | self.assertRaises(i3.MessageTypeError, i3.parse_msg_type, str(val)) 34 | 35 | def test_event_type_error(self): 36 | border_lower = -1 37 | border_higher = len(i3.EVENT_TYPES) 38 | values = ['joke', border_lower, border_higher, -100, 100] 39 | for val in values: 40 | self.assertRaises(i3.EventTypeError, i3.parse_event_type, val) 41 | self.assertRaises(i3.EventTypeError, i3.parse_event_type, str(val)) 42 | 43 | def test_msg_error(self): 44 | """If i3.yada doesn't pass, see http://bugs.i3wm.org/report/ticket/693""" 45 | self.assertRaises(i3.MessageError, i3.focus) # missing argument 46 | self.assertRaises(i3.MessageError, i3.yada) # doesn't exist 47 | self.assertRaises(i3.MessageError, i3.meh, 'some', 'args') 48 | 49 | 50 | class SocketTest(unittest.TestCase): 51 | def setUp(self): 52 | pass 53 | 54 | def test_connection(self): 55 | def connect(): 56 | return i3.Socket('/nil/2971.socket') 57 | self.assertRaises(i3.ConnectionError, connect) 58 | 59 | def test_response(self, socket=i3.default_socket()): 60 | workspaces = socket.get('get_workspaces') 61 | self.assertIsNotNone(workspaces) 62 | for workspace in workspaces: 63 | self.assertTrue('name' in workspace) 64 | 65 | def test_multiple_sockets(self): 66 | socket1 = i3.Socket() 67 | socket2 = i3.Socket() 68 | socket3 = i3.Socket() 69 | for socket in [socket1, socket2, socket3]: 70 | self.test_response(socket) 71 | for socket in [socket1, socket2, socket3]: 72 | socket.close() 73 | 74 | def test_pack(self): 75 | packed = i3.default_socket().pack(0, "haha") 76 | if py3: 77 | self.assertIsInstance(packed, bytes) 78 | 79 | 80 | class GeneralTest(unittest.TestCase): 81 | def setUp(self): 82 | pass 83 | 84 | def test_getattr(self): 85 | func = i3.some_attribute 86 | self.assertTrue(callable(func)) 87 | socket = i3.default_socket() 88 | self.assertIsInstance(socket, i3.Socket) 89 | 90 | def test_success(self): 91 | data = {'success': True} 92 | self.assertEqual(i3.success(data), True) 93 | self.assertEqual(i3.success([data, {'success': False}]), [True, False]) 94 | data = {'success': False, 'error': 'Error message'} 95 | self.assertIsInstance(i3.success(data), i3.MessageError) 96 | 97 | def test_container(self): 98 | container = i3.container(title='abc', con_id=123) 99 | output = ['[title="abc" con_id="123"]', 100 | '[con_id="123" title="abc"]'] 101 | self.assertTrue(container in output) 102 | 103 | def test_criteria(self): 104 | self.assertTrue(i3.focus(clasS='xterm')) 105 | 106 | def test_filter1(self): 107 | windows = i3.filter(nodes=[]) 108 | for window in windows: 109 | self.assertEqual(window['nodes'], []) 110 | 111 | def test_filter2(self): 112 | unfocused_windows = i3.filter(focused=False) 113 | parent_count = 0 114 | for window in unfocused_windows: 115 | self.assertEqual(window['focused'], False) 116 | if window['nodes'] != []: 117 | parent_count += 1 118 | self.assertGreater(parent_count, 0) 119 | 120 | def test_filter_function_wikipedia(self): 121 | """You have to have a Wikipedia tab opened in a browser.""" 122 | func = lambda node: 'Wikipedia' in node['name'] 123 | nodes = i3.filter(function=func) 124 | self.assertTrue(nodes != []) 125 | for node in nodes: 126 | self.assertTrue('free encyclopedia' in node['name']) 127 | 128 | if __name__ == '__main__': 129 | test_suits = [] 130 | for Test in [ParseTest, SocketTest, GeneralTest]: 131 | test_suits.append(unittest.TestLoader().loadTestsFromTestCase(Test)) 132 | unittest.TextTestRunner(verbosity=2).run(unittest.TestSuite(test_suits)) 133 | 134 | --------------------------------------------------------------------------------