├── screenshot.png ├── matei3applet.service ├── matei3applet.mate-panel-applet ├── log.py ├── setup.py ├── mate_version.py ├── LICENSE ├── .gitignore ├── i3conn.py ├── README.md ├── matei3applet.py └── i3ipc.py /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/city41/mate-i3-applet/HEAD/screenshot.png -------------------------------------------------------------------------------- /matei3applet.service: -------------------------------------------------------------------------------- 1 | [D-BUS Service] 2 | Name=org.mate.panel.applet.I3AppletFactory 3 | Exec=/usr/lib/mate-i3-applet/matei3applet.py 4 | -------------------------------------------------------------------------------- /matei3applet.mate-panel-applet: -------------------------------------------------------------------------------- 1 | [Applet Factory] 2 | Id=I3AppletFactory 3 | InProcess=false 4 | Location=/usr/lib/mate-i3-applet/matei3applet.pt 5 | Name=I3 Applet Factory 6 | Description=I3 Applet Factory 7 | 8 | [I3Applet] 9 | Name=i3 Workspaces 10 | Description=Shows i3 workspaces 11 | Icon=mate 12 | MateComponentId=OAFIID:MATE_I3Applet; 13 | 14 | -------------------------------------------------------------------------------- /log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | from logging import handlers 4 | import os 5 | 6 | 7 | def exception_handler(type, value, traceback): 8 | logging.exception( 9 | "Uncaught exception occurred: {}" 10 | .format(value) 11 | ) 12 | 13 | 14 | def setup_logging(): 15 | logger = logging.getLogger("") 16 | logger.setLevel(logging.WARNING) 17 | file_handler = handlers.TimedRotatingFileHandler( 18 | os.path.expanduser("~/.mate-i3-applet.log"), 19 | when="D", 20 | backupCount=1, 21 | delay=True, 22 | ) 23 | file_handler.setFormatter( 24 | logging.Formatter( 25 | '[%(levelname)s] %(asctime)s: %(message)s', 26 | "%Y-%m-%d %H:%M:%S", 27 | ) 28 | ) 29 | logger.addHandler(file_handler) 30 | sys.excepthook = exception_handler 31 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | 4 | #* a simple i3 workspace applet for MATE Desktop 5 | #* Copyright (c) 2017 Matt Greer 6 | #* see README for more information 7 | 8 | 9 | from distutils.core import setup 10 | from distutils.command.install_data import install_data 11 | 12 | 13 | class InstallData(install_data): 14 | def run(self): 15 | install_data.run(self) 16 | 17 | 18 | setup( 19 | name="mate-i3-applet", 20 | version="2.3.0", 21 | description="MATE i3 Workspace Applet", 22 | long_description="Applet for MATE Panel showing i3 workspaces and mode.", 23 | license="BSD", 24 | url="https://github.com/city41/mate-i3-applet", 25 | author="Matt Greer", 26 | author_email="matt.e.greer@gmail.com", 27 | 28 | data_files=[ 29 | ('/usr/lib/mate-i3-applet', [ 30 | 'matei3applet.py', 31 | 'log.py', 32 | 'i3conn.py', 33 | 'i3ipc.py', 34 | 'mate_version.py', 35 | ]), 36 | ('/usr/share/dbus-1/services', ['matei3applet.service']), 37 | ('/usr/share/mate-panel/applets', ['matei3applet.mate-panel-applet']), 38 | ], 39 | cmdclass={'install_data': InstallData} 40 | ) 41 | -------------------------------------------------------------------------------- /mate_version.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | import subprocess 4 | import sys 5 | from collections import namedtuple 6 | 7 | MateVersion = namedtuple("MateVersion", ["major", "minor", "patch"]) 8 | pattern = re.compile( 9 | b"(?P\d+)\." 10 | b"(?P\d+)\." 11 | b"(?P\d+)" 12 | ) 13 | 14 | 15 | def get_mate_version(): 16 | """ 17 | Return namedtuple with major, minor and patch version of Mate 18 | or None if Mate is not installed. 19 | """ 20 | 21 | try: 22 | mate_about_output = subprocess.check_output( 23 | ("mate-about", "--version") 24 | ) 25 | except FileNotFoundError: 26 | logging.error("command mate-about was not found") 27 | return None 28 | match = pattern.search(mate_about_output) 29 | return (MateVersion( 30 | major=int(match.group("major")), 31 | minor=int(match.group("minor")), 32 | patch=int(match.group("patch")), 33 | )) 34 | 35 | 36 | def import_gtk(): 37 | import gi 38 | 39 | version = get_mate_version() 40 | if version and version.major < 2 and version.minor < 16: 41 | gi.require_version("Gtk", "2.0") 42 | logging.debug("GTK 2.0 loaded") 43 | elif version: 44 | gi.require_version("Gtk", "3.0") 45 | logging.debug("GTK 3.0 loaded") 46 | else: 47 | logging.error("MATE is not installed, stopping execution..") 48 | sys.exit(1) 49 | gi.require_version('MatePanelApplet', '4.0') 50 | 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Tony Crisci 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | .static_storage/ 56 | .media/ 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | log.txt 106 | -------------------------------------------------------------------------------- /i3conn.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import threading 3 | import time 4 | 5 | import i3ipc 6 | 7 | 8 | class WorkspaceSub(threading.Thread): 9 | def __init__(self, con, callback, modeCallback): 10 | self.con = con 11 | 12 | i3callback = lambda _, workspaces: callback(self.con.get_workspaces()) 13 | i3ModeCallback = lambda _, mode: modeCallback(mode) 14 | self.con.on('workspace', i3callback) 15 | self.con.on('mode', i3ModeCallback) 16 | 17 | threading.Thread.__init__(self) 18 | self.start() 19 | 20 | def run(self): 21 | logging.debug('run') 22 | self.con.event_socket_setup() 23 | 24 | while not self.con.event_socket_poll(): 25 | logging.debug('loop') 26 | 27 | class I3Conn(object): 28 | def __init__(self): 29 | self.try_to_connect() 30 | 31 | def try_to_connect(self, tries=5): 32 | con = None 33 | while not con and tries > 0: 34 | try: 35 | con = self.create_connection() 36 | except: 37 | tries -= 1 38 | time.sleep(0.3) 39 | 40 | if not con: 41 | raise "Failed to connect to i3, is it running?" 42 | else: 43 | self.con = con 44 | 45 | def create_connection(self): 46 | logging.debug('I3Conn create_connection') 47 | con = i3ipc.Connection() 48 | con.on('ipc_shutdown', self.restart) 49 | return con 50 | 51 | def get_workspaces(self): 52 | return self.con.get_workspaces() 53 | 54 | def get_bar_config_list(self): 55 | return self.con.get_bar_config_list() 56 | 57 | def get_bar_config(self, bar_id): 58 | return self.con.get_bar_config(bar_id) 59 | 60 | # TODO: this is a hack to get workspace switching working. 61 | # The problem is listening to i3 events needs to be on its own 62 | # thread because it constantly blocks. So that means sending a command 63 | # is taking place on the main thread(s), I suspect GTK spins up more than 64 | # one thread or uses a thread pool. So the gtk thread(s) can't use the same 65 | # connection, as they will trip over themselves and 66 | # clobber the socket. The hack is to create a new connection each time, which 67 | # opens its own independent socket to i3. 68 | # 69 | # ways to fix this: 70 | # 1) figure out how to make this a critical section on the gtk thread 71 | # 2) spin up a third thread who's job is to send commands to i3, use a queue 72 | # 3) figure out how to use coroutines and ditch threads altogether 73 | # 74 | # to anyone reading this, I'm brand new to python, which is why I'm stumbling on this 75 | # 76 | def go_to_workspace(self, workspace_name): 77 | logging.debug('go to workspace: {}'.format(workspace_name)) 78 | throwawayCon = i3ipc.Connection() 79 | throwawayCon.command('workspace ' + workspace_name) 80 | throwawayCon.close() 81 | 82 | def subscribe(self, callback, modeCallback): 83 | if not self.con: 84 | raise "subscribing but there is no connection" 85 | self.callback = callback 86 | self.modeCallback = modeCallback 87 | self.sub = WorkspaceSub(self.con, self.callback, self.modeCallback) 88 | 89 | def close(self): 90 | logging.debug('I3Conn close') 91 | if self.con: 92 | self.con.close() 93 | self.con = None 94 | 95 | def restart(self, data=None): 96 | logging.debug('I3Conn restart') 97 | self.close() 98 | 99 | self.try_to_connect() 100 | 101 | if self.con and self.callback and self.modeCallback: 102 | self.subscribe(self.callback, self.modeCallback) 103 | 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MATE i3 Workspace Applet 2 | 3 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 4 | 5 | 6 | ![screenshot](https://raw.github.com/city41/mate-i3-applet/master/screenshot.png) 7 | 8 | This applet shows the current state of i3 workspaces when using i3 as your window manager in MATE. 9 | 10 | # Unsupported! (as of Aug 15, 2019) 11 | 12 | I am sorry but I am not supporting this applet. I built it because it met a need I have. But the truth is I'm not a very good Python programmer, nor do I really know much at all about mate internals or various Linux distros. I don't have the time to learn these things unfortunately. 13 | 14 | If anyone is interested in taking over, please let me know by creating an issue here in the repo 15 | 16 | ## Features 17 | 1. shows all workspaces in a similar fashion as i3bar 18 | 2. urgent workspaces highlight same as i3bar 19 | 3. shows modes such as "resize" 20 | 4. clicking a workspace goes to that workspace 21 | 5. uses the same colors as your i3bar in your i3 config (see below) 22 | 6. robust, reconnects if i3 relaunches 23 | 24 | ## Tested On 25 | 26 | * Ubuntu MATE 16.04 with MATE 1.12.1 and i3 4.11 (However no longer supporting GTK 2, see below) 27 | * Ubuntu MATE 17.10 with MATE 1.18.0 and i3 4.13 28 | * Ubuntu MATE 18.04 with MATE 1.20.1 29 | 30 | There has been [one report](https://github.com/city41/mate-i3-applet/issues/11#issuecomment-431692546) of the applet not working on Ubuntu MATE 18.10 31 | 32 | ### Other Distros 33 | 34 | **Debian:** It has been reported to work on Debian 10 after installing `gir1.2-matepanelapplet-4.0` package. Discussion [here](https://github.com/city41/mate-i3-applet/issues/23) 35 | 36 | ## Only supporting GTK3 37 | 38 | MATE 1.18.0 made the switch to GTK. This applet checks MATE version and imports appropriate version of GTK, 39 | however if you use GTK2 and encounter an issue, please upgrade before creating an issue. GTK2 specific issues 40 | will not be addressed. 41 | 42 | ## How to Install 43 | 44 | Requires Python 3 (minimum 3.5), no other dependencies 45 | 46 | 1. Grab the most recent [release](https://github.com/city41/mate-i3-applet/releases) 47 | 2. `sudo ./setup.py install` 48 | 3. `killall mate-panel` - this should kill then bring mate panel back, and it will now know about the i3 applet 49 | 4. Right click a panel, choose 'Add to panel...' and add the i3 applet 50 | 51 | ## How to setup i3 for this applet 52 | 53 | The point of this applet is to use a MATE panel instead of an i3 bar. But, this applet also reads your bar config to determine the colors to use. This is not a catch-22, as i3 allows you to define invisible bars. So in your i3 config, define one bar like so: 54 | 55 | ``` 56 | bar { 57 | tray_output None 58 | mode invisible 59 | colors { 60 | background #000000 61 | statusline #ffffff 62 | separator #666666 63 | 64 | focused_workspace #4c7899 #285577 #ffffff 65 | active_workspace #333333 #5f676a #ffffff 66 | inactive_workspace #333333 #222222 #888888 67 | urgent_workspace #2f343a #900000 #ffffff 68 | binding_mode #2f343a #900000 #ffffff 69 | } 70 | } 71 | ``` 72 | 73 | `tray_output None`: tells the bar to not accept tray icons. This allows them to go to the MATE panel. 74 | 75 | `mode invisible`: means the bar is invisible and never shown. We really only want the bar for its colors... 76 | 77 | `colors`: define the colors you want here. The applet will use these colors. Check the [i3 user guide](https://i3wm.org/docs/userguide.html#_colors) for more info on how to specify colors. 78 | 79 | ### If you just want default colors 80 | 81 | If you don't define a bar, or your bar doesn't have any colors defined, then the applet will use i3's default colors. Incidentally, the bar example above is what the default colors are. 82 | 83 | ## Todo 84 | 85 | Check the github issues, tracking all known issues and work there 86 | 87 | ## License 88 | 89 | This applet is using the BSD license. I also copied i3-ipc's source over into this applet because I needed the latest version and its not yet published to PyPI. i3-ipc is also licensed under the BSD license. 90 | -------------------------------------------------------------------------------- /matei3applet.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import logging 4 | 5 | from log import setup_logging 6 | setup_logging() 7 | 8 | from mate_version import import_gtk 9 | import_gtk() 10 | 11 | from gi.repository import Gtk 12 | from gi.repository import GLib 13 | from gi.repository import MatePanelApplet 14 | 15 | from i3conn import I3Conn 16 | 17 | DEFAULT_COLORS = { 18 | 'background': '#000000', 19 | 'statusline': '#ffffff', 20 | 'separator': '#666666', 21 | 22 | 'binding_mode_border': '#2f343a', 23 | 'binding_mode_bg': '#900000', 24 | 'binding_mode_text': '#ffffff', 25 | 26 | 'active_workspace_border': '#333333', 27 | 'active_workspace_bg': '#5f676a', 28 | 'active_workspace_text': '#ffffff', 29 | 30 | 'inactive_workspace_border': '#333333', 31 | 'inactive_workspace_bg': '#222222', 32 | 'inactive_workspace_text': '#888888', 33 | 34 | 'urgent_workspace_border': '#2f343a', 35 | 'urgent_workspace_bg': '#900000', 36 | 'urgent_workspace_text': '#ffffff', 37 | 38 | 'focused_workspace_border': '#4c7899', 39 | 'focused_workspace_bg': '#285577', 40 | 'focused_workspace_text': '#ffffff' 41 | } 42 | 43 | class i3bar(object): 44 | def __init__(self, applet): 45 | logging.debug("initializing mate-i3-applet") 46 | self.applet = applet 47 | self.applet.connect("destroy", self.destroy) 48 | self.i3conn = I3Conn() 49 | 50 | self.colors = self.init_colors() 51 | logging.debug('colors: {}'.format(str(self.colors))) 52 | 53 | self.init_widgets() 54 | self.set_initial_buttons() 55 | 56 | self.open_sub() 57 | 58 | def __del__(self): 59 | self.destroy(None) 60 | 61 | def destroy(self, event): 62 | self.close_sub() 63 | 64 | def init_widgets(self): 65 | self.box = Gtk.HBox() 66 | self.applet.add(self.box) 67 | self.modeLabel = Gtk.Label('') 68 | self.modeLabel.set_use_markup(True) 69 | 70 | def set_initial_buttons(self): 71 | workspaces = self.i3conn.get_workspaces() 72 | workspaces = sorted(workspaces, key = lambda i: i['num']) 73 | self.set_workspace_buttons(workspaces) 74 | 75 | def init_colors(self): 76 | global DEFAULT_COLORS 77 | 78 | bar_ids = self.i3conn.get_bar_config_list() 79 | 80 | colors = None 81 | while not colors and bar_ids: 82 | bar_id = bar_ids.pop() 83 | bar = self.i3conn.get_bar_config(bar_id) 84 | colors = bar['colors'] 85 | 86 | return colors or DEFAULT_COLORS 87 | 88 | def close_sub(self): 89 | logging.debug('close_sub') 90 | self.i3conn.close() 91 | 92 | def open_sub(self): 93 | logging.debug('open_sub') 94 | self.i3conn.subscribe(self.on_workspace_event, self.on_mode_event) 95 | 96 | def on_workspace_event(self, workspaces): 97 | logging.debug('on_workspace_event') 98 | 99 | if workspaces: 100 | GLib.idle_add(self.set_workspace_buttons, workspaces) 101 | 102 | def on_mode_event(self, mode): 103 | logging.debug('on_mode_event') 104 | logging.debug(mode.change) 105 | 106 | GLib.idle_add(self.set_mode_label_text, mode.change) 107 | 108 | def set_mode_label_text(self, text): 109 | if text == 'default': 110 | self.modeLabel.set_text('') 111 | elif all(key in self.colors for key in ('binding_mode_border','binding_mode_bg','binding_mode_text')): 112 | textToSet = ' %s ' % (self.colors['binding_mode_bg'], self.colors['binding_mode_text'], text) 113 | self.modeLabel.set_text(textToSet) 114 | else: 115 | textToSet = ' %s ' % (self.colors['urgent_workspace_bg'], self.colors['urgent_workspace_text'], text) 116 | self.modeLabel.set_text(textToSet) 117 | 118 | self.modeLabel.set_use_markup(True) 119 | self.modeLabel.show() 120 | 121 | def go_to_workspace(self, workspace): 122 | if not workspace['focused']: 123 | self.i3conn.go_to_workspace(workspace['name']) 124 | 125 | def set_workspace_buttons(self, workspaces): 126 | logging.debug('set_workspace_buttons') 127 | workspaces = sorted(workspaces, key = lambda i: i['num']) 128 | 129 | for child in self.box.get_children(): 130 | self.box.remove(child) 131 | 132 | def get_workspace_bgcolor(workspace): 133 | if workspace['urgent']: 134 | return self.colors['urgent_workspace_bg'] 135 | if workspace['focused']: 136 | return self.colors['focused_workspace_bg'] 137 | return self.colors['active_workspace_bg'] 138 | 139 | def get_workspace_fgcolor(workspace): 140 | if workspace['urgent']: 141 | return self.colors['urgent_workspace_text'] 142 | if workspace['focused']: 143 | return self.colors['focused_workspace_text'] 144 | return self.colors['active_workspace_text'] 145 | 146 | def workspace_to_label(workspace): 147 | bgcolor = get_workspace_bgcolor(workspace) 148 | fgcolor = get_workspace_fgcolor(workspace) 149 | return ' %s ' % (bgcolor, fgcolor, workspace['name']) 150 | 151 | def get_button(workspace): 152 | button = Gtk.EventBox() 153 | label = Gtk.Label(workspace_to_label(workspace)) 154 | label.set_use_markup(True) 155 | button.add(label) 156 | button.connect("button_press_event", lambda w,e: self.go_to_workspace(workspace)) 157 | return button 158 | 159 | for workspace in workspaces: 160 | self.box.pack_start(get_button(workspace), False, False, 0) 161 | 162 | self.box.pack_start(self.modeLabel, False, False, 0) 163 | self.box.show_all() 164 | 165 | def show(self): 166 | self.applet.show_all() 167 | 168 | def applet_factory(applet, iid, data): 169 | logging.debug('iid: {}'.format(iid)) 170 | if iid != "I3Applet": 171 | return False 172 | 173 | bar = i3bar(applet) 174 | bar.show() 175 | 176 | return True 177 | 178 | MatePanelApplet.Applet.factory_main("I3AppletFactory", True, 179 | MatePanelApplet.Applet.__gtype__, 180 | applet_factory, None) 181 | 182 | -------------------------------------------------------------------------------- /i3ipc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import errno 4 | import struct 5 | import json 6 | import socket 7 | import os 8 | import re 9 | import subprocess 10 | from enum import Enum 11 | from collections import deque 12 | 13 | 14 | class MessageType(Enum): 15 | COMMAND = 0 16 | GET_WORKSPACES = 1 17 | SUBSCRIBE = 2 18 | GET_OUTPUTS = 3 19 | GET_TREE = 4 20 | GET_MARKS = 5 21 | GET_BAR_CONFIG = 6 22 | GET_VERSION = 7 23 | 24 | 25 | class Event(object): 26 | WORKSPACE = (1 << 0) 27 | OUTPUT = (1 << 1) 28 | MODE = (1 << 2) 29 | WINDOW = (1 << 3) 30 | BARCONFIG_UPDATE = (1 << 4) 31 | BINDING = (1 << 5) 32 | 33 | 34 | class _ReplyType(dict): 35 | 36 | def __getattr__(self, name): 37 | return self[name] 38 | 39 | def __setattr__(self, name, value): 40 | self[name] = value 41 | 42 | def __delattr__(self, name): 43 | del self[name] 44 | 45 | 46 | class CommandReply(_ReplyType): 47 | """ 48 | Info about a command that was executed with :func:`Connection.command`. 49 | """ 50 | def __init__(self, data): 51 | super(CommandReply, self).__init__(data) 52 | 53 | @property 54 | def error(self): 55 | """ 56 | A human-readable error message 57 | 58 | :type: str 59 | """ 60 | return self.__getattr__('error') 61 | 62 | @property 63 | def success(self): 64 | """ 65 | Whether the command executed successfully 66 | 67 | :type: bool 68 | """ 69 | return self.__getattr__('success') 70 | 71 | 72 | class VersionReply(_ReplyType): 73 | """ 74 | Info about the version of the running i3 instance. 75 | """ 76 | def __init__(self, data): 77 | super(VersionReply, self).__init__(data) 78 | 79 | @property 80 | def major(self): 81 | """ 82 | The major version of i3. 83 | 84 | :type: int 85 | """ 86 | return self.__getattr__('major') 87 | 88 | @property 89 | def minor(self): 90 | """ 91 | The minor version of i3. 92 | 93 | :type: int 94 | """ 95 | return self.__getattr__('minor') 96 | 97 | @property 98 | def patch(self): 99 | """ 100 | The patch version of i3. 101 | 102 | :type: int 103 | """ 104 | return self.__getattr__('patch') 105 | 106 | @property 107 | def human_readable(self): 108 | """ 109 | A human-readable version of i3 containing the precise git version, 110 | build date, and branch name. 111 | 112 | :type: str 113 | """ 114 | return self.__getattr__('human_readable') 115 | 116 | @property 117 | def loaded_config_file_name(self): 118 | """ 119 | The current config path. 120 | 121 | :type: str 122 | """ 123 | return self.__getattr__('loaded_config_file_name') 124 | 125 | class BarConfigReply(_ReplyType): 126 | """ 127 | This can be used by third-party workspace bars (especially i3bar, but 128 | others are free to implement compatible alternatives) to get the bar block 129 | configuration from i3. 130 | 131 | Not all properties are documented here. A complete list of properties of 132 | this reply type can be found `here 133 | `_. 134 | """ 135 | def __init__(self, data): 136 | super(BarConfigReply, self).__init__(data) 137 | 138 | @property 139 | def colors(self): 140 | """ 141 | Contains key/value pairs of colors. Each value is a color code in hex, 142 | formatted #rrggbb (like in HTML). 143 | 144 | :type: dict 145 | """ 146 | return self.__getattr__('colors') 147 | 148 | @property 149 | def id(self): 150 | """ 151 | The ID for this bar. 152 | 153 | :type: str 154 | """ 155 | return self.__getattr__('id') 156 | 157 | @property 158 | def mode(self): 159 | """ 160 | Either ``dock`` (the bar sets the dock window type) or ``hide`` (the 161 | bar does not show unless a specific key is pressed). 162 | 163 | :type: str 164 | """ 165 | return self.__getattr__('mode') 166 | 167 | @property 168 | def position(self): 169 | """ 170 | Either ``bottom`` or ``top``. 171 | 172 | :type: str 173 | """ 174 | return self.__getattr__('position') 175 | 176 | @property 177 | def status_command(self): 178 | """ 179 | Command which will be run to generate a statusline. Each line on 180 | stdout of this command will be displayed in the bar. At the moment, no 181 | formatting is supported. 182 | 183 | :type: str 184 | """ 185 | return self.__getattr__('status_command') 186 | 187 | @property 188 | def font(self): 189 | """ 190 | The font to use for text on the bar. 191 | 192 | :type: str 193 | """ 194 | return self.__getattr__('font') 195 | 196 | 197 | 198 | class OutputReply(_ReplyType): 199 | pass 200 | 201 | 202 | class WorkspaceReply(_ReplyType): 203 | pass 204 | 205 | 206 | class WorkspaceEvent(object): 207 | 208 | def __init__(self, data, conn): 209 | self.change = data['change'] 210 | self.current = None 211 | self.old = None 212 | 213 | if 'current' in data and data['current']: 214 | self.current = Con(data['current'], None, conn) 215 | 216 | if 'old' in data and data['old']: 217 | self.old = Con(data['old'], None, conn) 218 | 219 | 220 | class GenericEvent(object): 221 | 222 | def __init__(self, data): 223 | self.change = data['change'] 224 | 225 | 226 | class WindowEvent(object): 227 | 228 | def __init__(self, data, conn): 229 | self.change = data['change'] 230 | self.container = Con(data['container'], None, conn) 231 | 232 | 233 | class BarconfigUpdateEvent(object): 234 | 235 | def __init__(self, data): 236 | self.id = data['id'] 237 | self.hidden_state = data['hidden_state'] 238 | self.mode = data['mode'] 239 | 240 | 241 | class BindingInfo(object): 242 | 243 | def __init__(self, data): 244 | self.command = data['command'] 245 | self.mods = data['mods'] 246 | self.input_code = data['input_code'] 247 | self.symbol = data['symbol'] 248 | self.input_type = data['input_type'] 249 | 250 | 251 | class BindingEvent(object): 252 | 253 | def __init__(self, data): 254 | self.change = data['change'] 255 | self.binding = BindingInfo(data['binding']) 256 | 257 | 258 | class _PubSub(object): 259 | 260 | def __init__(self, conn): 261 | self.conn = conn 262 | self._subscriptions = [] 263 | 264 | def subscribe(self, detailed_event, handler): 265 | event = detailed_event.replace('-', '_') 266 | detail = '' 267 | 268 | if detailed_event.count('::') > 0: 269 | [event, detail] = detailed_event.split('::') 270 | 271 | self._subscriptions.append({'event': event, 'detail': detail, 272 | 'handler': handler}) 273 | 274 | def emit(self, event, data): 275 | detail = '' 276 | 277 | if data and hasattr(data, 'change'): 278 | detail = data.change 279 | 280 | for s in self._subscriptions: 281 | if s['event'] == event: 282 | if not s['detail'] or s['detail'] == detail: 283 | if data: 284 | s['handler'](self.conn, data) 285 | else: 286 | s['handler'](self.conn) 287 | 288 | # this is for compatability with i3ipc-glib 289 | 290 | 291 | class _PropsObject(object): 292 | 293 | def __init__(self, obj): 294 | object.__setattr__(self, "_obj", obj) 295 | 296 | def __getattribute__(self, name): 297 | return getattr(object.__getattribute__(self, "_obj"), name) 298 | 299 | def __delattr__(self, name): 300 | delattr(object.__getattribute__(self, "_obj"), name) 301 | 302 | def __setattr__(self, name, value): 303 | setattr(object.__getattribute__(self, "_obj"), name, value) 304 | 305 | 306 | class Connection(object): 307 | """ 308 | This class controls a connection to the i3 ipc socket. It is capable of 309 | executing commands, subscribing to window manager events, and querying the 310 | window manager for information about the current state of windows, 311 | workspaces, outputs, and the i3bar. For more information, see the `ipc 312 | documentation `_ 313 | 314 | :param str socket_path: The path for the socket to the current i3 session. 315 | In most situations, you will not have to supply this yourself. Guessing 316 | first happens by the environment variable :envvar:`I3SOCK`, and, if this is 317 | empty, by executing :command:`i3 --get-socketpath`. 318 | :raises Exception: If the connection to ``i3`` cannot be established, or when 319 | the connection terminates. 320 | """ 321 | MAGIC = 'i3-ipc' # safety string for i3-ipc 322 | _chunk_size = 1024 # in bytes 323 | _timeout = 0.5 # in seconds 324 | _struct_header = '=%dsII' % len(MAGIC.encode('utf-8')) 325 | _struct_header_size = struct.calcsize(_struct_header) 326 | 327 | def __init__(self, socket_path=None): 328 | if not socket_path: 329 | socket_path = os.environ.get("I3SOCK") 330 | 331 | if not socket_path: 332 | try: 333 | socket_path = subprocess.check_output( 334 | ['i3', '--get-socketpath'], 335 | close_fds=True, universal_newlines=True).strip() 336 | except: 337 | raise Exception('Failed to retrieve the i3 IPC socket path') 338 | 339 | self._pubsub = _PubSub(self) 340 | self.props = _PropsObject(self) 341 | self.subscriptions = 0 342 | self.socket_path = socket_path 343 | self.cmd_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 344 | self.cmd_socket.connect(self.socket_path) 345 | self.sub_socket = None 346 | 347 | def close(self): 348 | self.event_socket_teardown() 349 | self.cmd_socket_teardown() 350 | 351 | def cmd_socket_teardown(self): 352 | if self.cmd_socket: 353 | self.cmd_socket.shutdown(socket.SHUT_WR) 354 | self.cmd_socket = None 355 | 356 | def _pack(self, msg_type, payload): 357 | """ 358 | Packs the given message type and payload. Turns the resulting 359 | message into a byte string. 360 | """ 361 | pb = payload.encode('utf-8') 362 | s = struct.pack('=II', len(pb), msg_type.value) 363 | return self.MAGIC.encode('utf-8') + s + pb 364 | 365 | def _unpack(self, data): 366 | """ 367 | Unpacks the given byte string and parses the result from JSON. 368 | Returns None on failure and saves data into "self.buffer". 369 | """ 370 | msg_magic, msg_length, msg_type = self._unpack_header(data) 371 | msg_size = self._struct_header_size + msg_length 372 | # XXX: Message shouldn't be any longer than the data 373 | payload = data[self._struct_header_size:msg_size] 374 | return payload.decode('utf-8', 'replace') 375 | 376 | def _unpack_header(self, data): 377 | """ 378 | Unpacks the header of given byte string. 379 | """ 380 | return struct.unpack(self._struct_header, 381 | data[:self._struct_header_size]) 382 | 383 | def _recv_robust(self, sock, size): 384 | """ 385 | Receive size from sock, and retry if the recv() call was interrupted. 386 | (this is only required for python2 compatability) 387 | """ 388 | while True: 389 | try: 390 | return sock.recv(size) 391 | except socket.error as e: 392 | if e.errno != errno.EINTR: 393 | raise 394 | 395 | def _ipc_recv(self, sock): 396 | data = self._recv_robust(sock, 14) 397 | 398 | if len(data) == 0: 399 | # EOF 400 | return '', 0 401 | 402 | msg_magic, msg_length, msg_type = self._unpack_header(data) 403 | msg_size = self._struct_header_size + msg_length 404 | while len(data) < msg_size: 405 | data += self._recv_robust(sock, msg_length) 406 | return self._unpack(data), msg_type 407 | 408 | def _ipc_send(self, sock, message_type, payload): 409 | sock.sendall(self._pack(message_type, payload)) 410 | data, msg_type = self._ipc_recv(sock) 411 | return data 412 | 413 | def message(self, message_type, payload): 414 | return self._ipc_send(self.cmd_socket, message_type, payload) 415 | 416 | def command(self, payload): 417 | """ 418 | Send a command to i3. See the `list of commands 419 | `_ in the user 420 | guide for available commands. Pass the text of the command to execute 421 | as the first arguments. This is essentially the same as using 422 | ``i3-msg`` or an ``exec`` block in your i3 config to control the 423 | window manager. 424 | 425 | :rtype: List of :class:`CommandReply`. 426 | """ 427 | data = self.message(MessageType.COMMAND, payload) 428 | return json.loads(data, object_hook=CommandReply) 429 | 430 | def get_version(self): 431 | """ 432 | Get json encoded information about the running i3 instance. The 433 | equivalent of :command:`i3-msg -t get_version`. The return 434 | object exposes the following attributes :attr:`~VersionReply.major`, 435 | :attr:`~VersionReply.minor`, :attr:`~VersionReply.patch`, 436 | :attr:`~VersionReply.human_readable`, and 437 | :attr:`~VersionReply.loaded_config_file_name`. 438 | 439 | Example output: 440 | 441 | .. code:: json 442 | 443 | {'patch': 0, 444 | 'human_readable': '4.12 (2016-03-06, branch "4.12")', 445 | 'major': 4, 446 | 'minor': 12, 447 | 'loaded_config_file_name': '/home/joep/.config/i3/config'} 448 | 449 | 450 | :rtype: VersionReply 451 | 452 | """ 453 | data = self.message(MessageType.GET_VERSION, '') 454 | return json.loads(data, object_hook=VersionReply) 455 | 456 | def get_bar_config(self, bar_id=None): 457 | """ 458 | Get the configuration of a single bar. Defaults to the first if none is 459 | specified. Use :meth:`get_bar_config_list` to obtain a list of valid 460 | IDs. 461 | 462 | :rtype: BarConfigReply 463 | """ 464 | if not bar_id: 465 | bar_config_list = self.get_bar_config_list() 466 | if not bar_config_list: 467 | return None 468 | bar_id = bar_config_list[0] 469 | 470 | data = self.message(MessageType.GET_BAR_CONFIG, bar_id) 471 | return json.loads(data, object_hook=BarConfigReply) 472 | 473 | def get_bar_config_list(self): 474 | """ 475 | Get list of bar IDs as active in the connected i3 session. 476 | 477 | :rtype: List of strings that can be fed as ``bar_id`` into 478 | :meth:`get_bar_config`. 479 | """ 480 | data = self.message(MessageType.GET_BAR_CONFIG, '') 481 | return json.loads(data) 482 | 483 | def get_outputs(self): 484 | """ 485 | Get a list of outputs. The equivalent of :command:`i3-msg -t get_outputs`. 486 | 487 | :rtype: List of :class:`OutputReply`. 488 | 489 | Example output: 490 | 491 | .. code:: python 492 | 493 | >>> i3ipc.Connection().get_outputs() 494 | [{'name': 'eDP1', 495 | 'primary': True, 496 | 'active': True, 497 | 'rect': {'width': 1920, 'height': 1080, 'y': 0, 'x': 0}, 498 | 'current_workspace': '2'}, 499 | {'name': 'xroot-0', 500 | 'primary': False, 501 | 'active': False, 502 | 'rect': {'width': 1920, 'height': 1080, 'y': 0, 'x': 0}, 503 | 'current_workspace': None}] 504 | """ 505 | data = self.message(MessageType.GET_OUTPUTS, '') 506 | return json.loads(data, object_hook=OutputReply) 507 | 508 | def get_workspaces(self): 509 | """ 510 | Get a list of workspaces. Returns JSON-like data, not a Con instance. 511 | 512 | You might want to try the :meth:`Con.workspaces` instead if the info 513 | contained here is too little. 514 | 515 | :rtype: List of :class:`WorkspaceReply`. 516 | 517 | """ 518 | data = self.message(MessageType.GET_WORKSPACES, '') 519 | return json.loads(data, object_hook=WorkspaceReply) 520 | 521 | def get_tree(self): 522 | """ 523 | Returns a :class:`Con` instance with all kinds of methods and selectors. 524 | Start here with exploration. Read up on the :class:`Con` stuffs. 525 | 526 | :rtype: Con 527 | """ 528 | data = self.message(MessageType.GET_TREE, '') 529 | return Con(json.loads(data), None, self) 530 | 531 | def subscribe(self, events): 532 | events_obj = [] 533 | if events & Event.WORKSPACE: 534 | events_obj.append("workspace") 535 | if events & Event.OUTPUT: 536 | events_obj.append("output") 537 | if events & Event.MODE: 538 | events_obj.append("mode") 539 | if events & Event.WINDOW: 540 | events_obj.append("window") 541 | if events & Event.BARCONFIG_UPDATE: 542 | events_obj.append("barconfig_update") 543 | if events & Event.BINDING: 544 | events_obj.append("binding") 545 | 546 | data = self._ipc_send( 547 | self.sub_socket, MessageType.SUBSCRIBE, json.dumps(events_obj)) 548 | result = json.loads(data, object_hook=CommandReply) 549 | self.subscriptions |= events 550 | return result 551 | 552 | def on(self, detailed_event, handler): 553 | event = detailed_event.replace('-', '_') 554 | 555 | if detailed_event.count('::') > 0: 556 | [event, __] = detailed_event.split('::') 557 | 558 | # special case: ipc-shutdown is not in the protocol 559 | if event == 'ipc_shutdown': 560 | self._pubsub.subscribe(event, handler) 561 | return 562 | 563 | event_type = 0 564 | if event == "workspace": 565 | event_type = Event.WORKSPACE 566 | elif event == "output": 567 | event_type = Event.OUTPUT 568 | elif event == "mode": 569 | event_type = Event.MODE 570 | elif event == "window": 571 | event_type = Event.WINDOW 572 | elif event == "barconfig_update": 573 | event_type = Event.BARCONFIG_UPDATE 574 | elif event == "binding": 575 | event_type = Event.BINDING 576 | 577 | if not event_type: 578 | raise Exception('event not implemented') 579 | 580 | self.subscriptions |= event_type 581 | 582 | self._pubsub.subscribe(detailed_event, handler) 583 | 584 | def event_socket_setup(self): 585 | self.sub_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 586 | self.sub_socket.connect(self.socket_path) 587 | 588 | self.subscribe(self.subscriptions) 589 | 590 | def event_socket_teardown(self): 591 | if self.sub_socket: 592 | self.sub_socket.shutdown(socket.SHUT_WR) 593 | self.sub_socket = None 594 | 595 | def event_socket_poll(self): 596 | if self.sub_socket is None: 597 | return True 598 | 599 | data, msg_type = self._ipc_recv(self.sub_socket) 600 | 601 | if len(data) == 0: 602 | # EOF 603 | self._pubsub.emit('ipc_shutdown', None) 604 | return True 605 | 606 | data = json.loads(data) 607 | msg_type = 1 << (msg_type & 0x7f) 608 | event_name = '' 609 | event = None 610 | 611 | if msg_type == Event.WORKSPACE: 612 | event_name = 'workspace' 613 | event = WorkspaceEvent(data, self) 614 | elif msg_type == Event.OUTPUT: 615 | event_name = 'output' 616 | event = GenericEvent(data) 617 | elif msg_type == Event.MODE: 618 | event_name = 'mode' 619 | event = GenericEvent(data) 620 | elif msg_type == Event.WINDOW: 621 | event_name = 'window' 622 | event = WindowEvent(data, self) 623 | elif msg_type == Event.BARCONFIG_UPDATE: 624 | event_name = 'barconfig_update' 625 | event = BarconfigUpdateEvent(data) 626 | elif msg_type == Event.BINDING: 627 | event_name = 'binding' 628 | event = BindingEvent(data) 629 | else: 630 | # we have not implemented this event 631 | return 632 | 633 | self._pubsub.emit(event_name, event) 634 | 635 | def main(self): 636 | self.event_socket_setup() 637 | 638 | while not self.event_socket_poll(): 639 | pass 640 | 641 | def main_quit(self): 642 | self.event_socket_teardown() 643 | 644 | 645 | class Rect(object): 646 | 647 | def __init__(self, data): 648 | self.x = data['x'] 649 | self.y = data['y'] 650 | self.height = data['height'] 651 | self.width = data['width'] 652 | 653 | class Gaps(object): 654 | 655 | def __init__(self,data): 656 | self.inner = data['inner'] 657 | self.outer = data['outer'] 658 | 659 | 660 | class Con(object): 661 | """ 662 | The container class. Has all internal information about the windows, 663 | outputs, workspaces and containers that :command:`i3` manages. 664 | 665 | .. attribute:: id 666 | 667 | The internal ID (actually a C pointer value within i3) of the container. 668 | You can use it to (re-)identify and address containers when talking to 669 | i3. 670 | 671 | .. attribute:: name 672 | 673 | The internal name of the container. ``None`` for containers which 674 | are not leaves. The string `_NET_WM_NAME <://specifications.freedesktop.org/wm-spec/1.3/ar01s05.html#idm140238712347280>`_ 675 | for windows. Read-only value. 676 | 677 | .. attribute:: type 678 | 679 | The type of the container. Can be one of ``root``, ``output``, ``con``, 680 | ``floating_con``, ``workspace`` or ``dockarea``. 681 | 682 | .. attribute:: title 683 | 684 | The window title. 685 | 686 | .. attribute:: window_class 687 | 688 | The window class. 689 | 690 | .. attribute:: instance 691 | 692 | The instance name of the window class. 693 | 694 | .. attribute:: gaps 695 | 696 | The inner and outer gaps devation from default values. 697 | 698 | .. attribute:: border 699 | 700 | The type of border style for the selected container. Can be either 701 | ``normal``, ``none`` or ``1pixel``. 702 | 703 | .. attribute:: current_border_width 704 | 705 | Returns amount of pixels for the border. Readonly value. See `i3's user 706 | manual _ 707 | for more info. 708 | 709 | .. attribute:: layout 710 | 711 | Can be either ``splith``, ``splitv``, ``stacked``, ``tabbed``, ``dockarea`` or 712 | ``output``. 713 | :rtype: string 714 | 715 | .. attribute:: percent 716 | 717 | The percentage which this container takes in its parent. A value of 718 | null means that the percent property does not make sense for this 719 | container, for example for the root container. 720 | :rtype: float 721 | 722 | .. attribute:: rect 723 | 724 | The absolute display coordinates for this container. Display 725 | coordinates means that when you have two 1600x1200 monitors on a single 726 | X11 Display (the standard way), the coordinates of the first window on 727 | the second monitor are ``{ "x": 1600, "y": 0, "width": 1600, "height": 728 | 1200 }``. 729 | 730 | .. attribute:: window_rect 731 | 732 | The coordinates of the *actual client window* inside the container, 733 | without the window decorations that may also occupy space. 734 | 735 | .. attribute:: deco_rect 736 | 737 | The coordinates of the window decorations within a container. The 738 | coordinates are relative to the container and do not include the client 739 | window. 740 | 741 | .. attribute:: geometry 742 | 743 | The original geometry the window specified when i3 mapped it. Used when 744 | switching a window to floating mode, for example. 745 | 746 | .. attribute:: window 747 | 748 | The X11 window ID of the client window. 749 | 750 | .. attribute:: focus 751 | 752 | A list of container ids describing the focus situation within the current 753 | container. The first element refers to the container with (in)active focus. 754 | 755 | .. attribute:: focused 756 | 757 | Whether or not the current container is focused. There is only 758 | one focused container. 759 | 760 | .. attribute:: visible 761 | 762 | Whether or not the current container is visible. 763 | 764 | .. attribute:: num 765 | 766 | Optional attribute that only makes sense for workspaces. This allows 767 | for arbitrary and changeable names, even though the keyboard 768 | shortcuts remain the same. See `the i3wm docs `_ 769 | for more information 770 | 771 | .. attribute:: urgent 772 | 773 | Whether the window or workspace has the `urgent` state. 774 | 775 | :returns: :bool:`True` or :bool:`False`. 776 | 777 | .. attribute:: floating 778 | 779 | Whether the container is floating or not. Possible values are 780 | "auto_on", "auto_off", "user_on" and "user_off" 781 | 782 | 783 | .. 784 | command <-- method 785 | command_children <-- method 786 | deco_rect IPC 787 | descendents 788 | find_by_id 789 | find_by_role 790 | find_by_window 791 | find_classed 792 | find_focused 793 | find_fullscreen 794 | find_marked 795 | find_named 796 | floating 797 | floating_nodes 798 | fullscreen_mode 799 | gaps 800 | leaves 801 | marks 802 | nodes 803 | orientation 804 | parent 805 | props 806 | root 807 | scratchpad 808 | scratchpad_state 809 | window_class 810 | window_instance 811 | window_rect 812 | window_role 813 | workspace 814 | workspaces 815 | 816 | 817 | """ 818 | 819 | def __init__(self, data, parent, conn): 820 | self.props = _PropsObject(self) 821 | self._conn = conn 822 | self.parent = parent 823 | 824 | # set simple properties 825 | ipc_properties = ['border', 'current_border_width', 'focus', 'focused', 826 | 'fullscreen_mode', 'id', 'layout', 'marks', 'name', 827 | 'orientation', 'percent', 'type', 'urgent', 'window', 828 | 'num', 'scratchpad_state'] 829 | for attr in ipc_properties: 830 | if attr in data: 831 | setattr(self, attr, data[attr]) 832 | else: 833 | setattr(self, attr, None) 834 | 835 | # XXX in 4.12, marks is an array (old property was a string "mark") 836 | if not self.marks: 837 | self.marks = [] 838 | if 'mark' in data and data['mark']: 839 | self.marks.append(data['mark']) 840 | 841 | # Possible values 'user_off', 'user_on', 'auto_off', 'auto_on' 842 | if data['floating']: 843 | self.floating = data['floating'] 844 | 845 | # XXX this is for compatability with 4.8 846 | if isinstance(self.type, int): 847 | if self.type == 0: 848 | self.type = "root" 849 | elif self.type == 1: 850 | self.type = "output" 851 | elif self.type == 2 or self.type == 3: 852 | self.type = "con" 853 | elif self.type == 4: 854 | self.type = "workspace" 855 | elif self.type == 5: 856 | self.type = "dockarea" 857 | 858 | # set complex properties 859 | self.nodes = [] 860 | for n in data['nodes']: 861 | self.nodes.append(Con(n, self, conn)) 862 | 863 | self.floating_nodes = [] 864 | for n in data['floating_nodes']: 865 | self.floating_nodes.append(Con(n, self, conn)) 866 | 867 | self.window_class = None 868 | self.window_instance = None 869 | self.window_role = None 870 | if 'window_properties' in data: 871 | if 'class' in data['window_properties']: 872 | self.window_class = data['window_properties']['class'] 873 | if 'instance' in data['window_properties']: 874 | self.window_instance = data['window_properties']['instance'] 875 | if 'window_role' in data['window_properties']: 876 | self.window_role = data['window_properties']['window_role'] 877 | 878 | self.rect = Rect(data['rect']) 879 | if 'window_rect' in data: 880 | self.window_rect = Rect(data['window_rect']) 881 | if 'deco_rect' in data: 882 | self.deco_rect = Rect(data['deco_rect']) 883 | 884 | self.gaps = None 885 | if 'gaps' in data: 886 | self.gaps = Gaps(data['gaps']) 887 | 888 | def __iter__(self): 889 | """ 890 | Iterate through the descendents of this node (breadth-first tree traversal) 891 | """ 892 | queue = deque(self.nodes) 893 | 894 | while queue: 895 | con = queue.popleft() 896 | yield con 897 | queue.extend(con.nodes) 898 | queue.extend(con.floating_nodes) 899 | 900 | def root(self): 901 | """ 902 | Retrieves the root container. 903 | 904 | :rtype: :class:`Con`. 905 | """ 906 | 907 | if not self.parent: 908 | return self 909 | 910 | con = self.parent 911 | 912 | while con.parent: 913 | con = con.parent 914 | 915 | return con 916 | 917 | def descendents(self): 918 | """ 919 | Retrieve a list of all containers that delineate from the currently 920 | selected container. Includes any kind of container. 921 | 922 | :rtype: List of :class:`Con`. 923 | """ 924 | return [c for c in self] 925 | 926 | def leaves(self): 927 | """ 928 | Retrieve a list of windows that delineate from the currently 929 | selected container. Only lists client windows, no intermediate 930 | containers. 931 | 932 | :rtype: List of :class:`Con`. 933 | """ 934 | leaves = [] 935 | 936 | for c in self: 937 | if not c.nodes and c.type == "con" and c.parent.type != "dockarea": 938 | leaves.append(c) 939 | 940 | return leaves 941 | 942 | def command(self, command): 943 | """ 944 | Run a command on the currently active container. 945 | 946 | :rtype: CommandReply 947 | """ 948 | return self._conn.command('[con_id="{}"] {}'.format(self.id, command)) 949 | 950 | def command_children(self, command): 951 | """ 952 | Run a command on the direct children of the currently selected 953 | container. 954 | 955 | :rtype: List of CommandReply???? 956 | """ 957 | if not len(self.nodes): 958 | return 959 | 960 | commands = [] 961 | for c in self.nodes: 962 | commands.append('[con_id="{}"] {};'.format(c.id, command)) 963 | 964 | self._conn.command(' '.join(commands)) 965 | 966 | def workspaces(self): 967 | """ 968 | Retrieve a list of currently active workspaces. 969 | 970 | :rtype: List of :class:`Con`. 971 | """ 972 | workspaces = [] 973 | 974 | def collect_workspaces(con): 975 | if con.type == "workspace" and not con.name.startswith('__'): 976 | workspaces.append(con) 977 | return 978 | 979 | for c in con.nodes: 980 | collect_workspaces(c) 981 | 982 | collect_workspaces(self.root()) 983 | return workspaces 984 | 985 | def find_focused(self): 986 | """ 987 | Finds the focused container. 988 | 989 | :rtype class Con: 990 | """ 991 | try: 992 | return next(c for c in self if c.focused) 993 | except StopIteration: 994 | return None 995 | 996 | def find_by_id(self, id): 997 | try: 998 | return next(c for c in self if c.id == id) 999 | except StopIteration: 1000 | return None 1001 | 1002 | def find_by_window(self, window): 1003 | try: 1004 | return next(c for c in self if c.window == window) 1005 | except StopIteration: 1006 | return None 1007 | 1008 | def find_by_role(self, pattern): 1009 | return [c for c in self 1010 | if c.window_role and re.search(pattern, c.window_role)] 1011 | 1012 | def find_named(self, pattern): 1013 | return [c for c in self 1014 | if c.name and re.search(pattern, c.name)] 1015 | 1016 | def find_classed(self, pattern): 1017 | return [c for c in self 1018 | if c.window_class and re.search(pattern, c.window_class)] 1019 | 1020 | def find_instanced(self, pattern): 1021 | return [c for c in self 1022 | if c.window_instance and re.search(pattern, c.window_instance)] 1023 | 1024 | def find_marked(self, pattern=".*"): 1025 | pattern = re.compile(pattern) 1026 | return [c for c in self 1027 | if any(pattern.search(mark) for mark in c.marks)] 1028 | 1029 | def find_fullscreen(self): 1030 | return [c for c in self 1031 | if c.type == 'con' and c.fullscreen_mode] 1032 | 1033 | def workspace(self): 1034 | if self.type == 'workspace': 1035 | return self 1036 | 1037 | ret = self.parent 1038 | 1039 | while ret: 1040 | if ret.type == 'workspace': 1041 | break 1042 | ret = ret.parent 1043 | 1044 | return ret 1045 | 1046 | def scratchpad(self): 1047 | root = self.root() 1048 | 1049 | i3con = None 1050 | for c in root.nodes: 1051 | if c.name == "__i3": 1052 | i3con = c 1053 | break 1054 | 1055 | if not i3con: 1056 | return None 1057 | 1058 | i3con_content = None 1059 | for c in i3con.nodes: 1060 | if c.name == "content": 1061 | i3con_content = c 1062 | break 1063 | 1064 | if not i3con_content: 1065 | return None 1066 | 1067 | scratch = None 1068 | for c in i3con_content.nodes: 1069 | if c.name == "__i3_scratch": 1070 | scratch = c 1071 | break 1072 | 1073 | return scratch 1074 | --------------------------------------------------------------------------------