Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
__pycache__/
*.pyc
.venv/
dist/

# Beads / Dolt / Claude tooling
.beads/
.claude/
.dolt/
*.db
.beads-credential-key
AGENTS.md
CLAUDE.md
1 change: 0 additions & 1 deletion MANIFEST.in

This file was deleted.

24 changes: 17 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ Install
-------

pip install i3-py
# OR/AND (for Python 2.x)
pip2 install i3-py


Run examples
Expand All @@ -35,7 +33,7 @@ Usage
The basics
----------

The communication with i3-wm is through sockets. There are 7 types of messages:
The communication with i3-wm is through sockets. There are 13 types of messages:

- command (0)
- get_workspaces (1)
Expand All @@ -44,14 +42,26 @@ The communication with i3-wm is through sockets. There are 7 types of messages:
- get_tree (4)
- get_marks (5)
- get_bar_config (6)
- get_version (7)
- get_binding_modes (8)
- get_config (9)
- send_tick (10)
- sync (11)
- get_binding_state (12)

You can control i3-wm with _command_ messages. Other message types return
information about i3-wm without changing its behaviour.

_Subscribe_ offers 2 event types (read: _changes_) to subscribe for:
_Subscribe_ offers 8 event types (read: _changes_) to subscribe for:

- workspace (0)
- output (1)
- mode (2)
- window (3)
- barconfig_update (4)
- binding (5)
- shutdown (6)
- tick (7)

There are various ways to do this with i3.py. Let's start with...

Expand Down Expand Up @@ -288,10 +298,10 @@ References:
- [i3-wm's user guide](http://i3wm.org/docs/userguide.html) contains lots of
commands that you can use with i3-py.

i3-py was tested with Python 3.2.2 and 2.7.2.
i3-py requires Python 3.10 or later.

Dependencies:

- i3-wm
- Python
- i3-wm (4.14+)
- Python (3.10+)

19 changes: 10 additions & 9 deletions examples/ipc.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def parse():
return args


def main(socket, type, timeout, message):
def main(socket, msg_type, timeout, message):
"""
Excepts arguments and evaluates them.
"""
Expand All @@ -68,14 +68,14 @@ def main(socket, type, timeout, message):
try:
i3.default_socket(i3.Socket(path=socket, timeout=timeout))
except i3.ConnectionError:
print("Couldn't connect to socket at '%s'." % socket)
print(f"Couldn't connect to socket at '{socket}'.")
return False
# Format input
if type in i3.EVENT_TYPES:
event_type = type
if msg_type in i3.EVENT_TYPES:
event_type = msg_type
event = message
type = 'subscribe'
elif type == 'subscribe':
msg_type = 'subscribe'
elif msg_type == 'subscribe':
message = message.split(' ')
message_len = len(message)
if message_len >= 1:
Expand All @@ -85,13 +85,13 @@ def main(socket, type, timeout, message):
else:
event = ''
else:
# Let if fail
# Let it fail
event_type = ''
try:
if type == 'subscribe':
if msg_type == 'subscribe':
i3.subscribe(event_type, event)
else:
output = i3.msg(type, message)
output = i3.msg(msg_type, message)
print(output)
except i3.i3Exception as i3error:
print(i3error)
Expand All @@ -101,3 +101,4 @@ def main(socket, type, timeout, message):
args = parse()
main(args.socket, args.type, args.timeout, args.message)


5 changes: 3 additions & 2 deletions examples/winmenu.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@ def win_menu(clients, l=10):
"""
dmenu = subprocess.Popen(['/usr/bin/dmenu','-i','-l', str(l)],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
stdout=subprocess.PIPE,
text=True)
menu_str = '\n'.join(sorted(clients.keys()))
# Popen.communicate returns a tuple stdout, stderr
win_str = dmenu.communicate(menu_str.encode('utf-8'))[0].decode('utf-8').rstrip()
win_str = dmenu.communicate(menu_str)[0].rstrip()
return clients.get(win_str, None)

if __name__ == '__main__':
Expand Down
12 changes: 4 additions & 8 deletions examples/wsbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import i3


class Colors(object):
class Colors:
"""
A class for easier managing of bar's colors.
Attributes (hexadecimal color values):
Expand Down Expand Up @@ -64,7 +64,7 @@ def get_color(self, workspace, output):
return self.inactive


class i3wsbar(object):
class i3wsbar:
"""
A workspace bar; display a list of workspaces using a given bar
application. Defaults to dzen2.
Expand Down Expand Up @@ -105,7 +105,7 @@ def __init__(self, colors=None, font=None, bar_cmd=None, bar_args=None):
outputs = self.socket.get('get_outputs')
self.display(self.format(workspaces, outputs))
# Subscribe to an event
callback = lambda data, event, _: self.change(data, event)
callback = lambda event, data, _: self.change(event, data)
self.subscription = i3.Subscription(callback, 'workspace')

def change(self, event, workspaces):
Expand Down Expand Up @@ -144,11 +144,7 @@ def display(self, bar_text):
Displays a text on the bar by piping it to the bar application.
"""
bar_text += '\n'
try:
bar_text = bar_text.encode()
except AttributeError:
pass # already a byte string
self.bar.stdin.write(bar_text)
self.bar.stdin.write(bar_text.encode())

def quit(self):
"""
Expand Down
80 changes: 48 additions & 32 deletions i3.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,12 @@
import struct
import threading
import time

ModuleType = type(sys)
import types


__author__ = 'Jure Ziberna'
__version__ = '0.6.5'
__date__ = '2012-06-20'
__version__ = '0.7.0'
__date__ = '2026-04-06'
__license__ = 'GNU GPL 3'


Expand All @@ -42,11 +41,23 @@
'get_tree',
'get_marks',
'get_bar_config',
'get_version',
'get_binding_modes',
'get_config',
'send_tick',
'sync',
'get_binding_state',
]

EVENT_TYPES = [
'workspace',
'output',
'mode',
'window',
'barconfig_update',
'binding',
'shutdown',
'tick',
]


Expand All @@ -58,16 +69,14 @@ class MessageTypeError(i3Exception):
Raised when message type isn't available. See i3.MSG_TYPES.
"""
def __init__(self, type):
msg = "Message type '%s' isn't available" % type
super(MessageTypeError, self).__init__(msg)
super().__init__(f"Message type '{type}' isn't available")

class EventTypeError(i3Exception):
"""
Raised when even type isn't available. See i3.EVENT_TYPES.
"""
def __init__(self, type):
msg = "Event type '%s' isn't available" % type
super(EventTypeError, self).__init__(msg)
super().__init__(f"Event type '{type}' isn't available")

class MessageError(i3Exception):
"""
Expand All @@ -81,8 +90,7 @@ class ConnectionError(i3Exception):
Raised when a socket couldn't connect to the window manager.
"""
def __init__(self, socket_path):
msg = "Could not connect to socket at '%s'" % socket_path
super(ConnectionError, self).__init__(msg)
super().__init__(f"Could not connect to socket at '{socket_path}'")


def parse_msg_type(msg_type):
Expand Down Expand Up @@ -120,7 +128,7 @@ def parse_event_type(event_type):
raise EventTypeError(event_type)


class Socket(object):
class Socket:
"""
Socket for communicating with the i3 window manager.
Optional arguments:
Expand All @@ -146,17 +154,23 @@ def __init__(self, path=None, timeout=None, chunk_size=None,
self.chunk_size = chunk_size
if magic_string:
self.magic_string = magic_string
# Socket initialization and connection
self.initialize()
self.connect()
# Struct format initialization, length of magic string is in bytes
self.struct_header = '<%dsII' % len(self.magic_string.encode('utf-8'))
self.struct_header_size = struct.calcsize(self.struct_header)
# Socket initialization and connection
self.initialize()
try:
self.connect()
except ConnectionError:
self.socket.close()
raise

def initialize(self):
"""
Initializes the socket.
Initializes the socket, closing any existing one first.
"""
if hasattr(self, 'socket'):
self.socket.close()
self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.socket.settimeout(self.timeout)

Expand All @@ -170,7 +184,7 @@ def connect(self, path=None):
path = self.path
try:
self.socket.connect(path)
except socket.error:
except OSError:
raise ConnectionError(path)

def get(self, msg_type, payload=''):
Expand Down Expand Up @@ -224,16 +238,11 @@ def pack(self, msg_type, payload):
Packs the given message type and payload. Turns the resulting
message into a byte string.
"""
msg_magic = self.magic_string
# Get the byte count instead of number of characters
msg_length = len(payload.encode('utf-8'))
payload_bytes = payload.encode('utf-8')
msg_type = parse_msg_type(msg_type)
# "struct.pack" returns byte string, decoding it for concatenation
msg_length = struct.pack('I', msg_length).decode('utf-8')
msg_type = struct.pack('I', msg_type).decode('utf-8')
message = '%s%s%s%s' % (msg_magic, msg_length, msg_type, payload)
# Encoding the message back to byte string
return message.encode('utf-8')
header = struct.pack(self.struct_header, self.magic_string.encode('utf-8'),
len(payload_bytes), msg_type)
return header + payload_bytes

def unpack(self, data):
"""
Expand Down Expand Up @@ -267,7 +276,7 @@ def connected(self):
try:
self.get('command')
return True
except socket.error:
except OSError:
return False

def close(self):
Expand All @@ -291,7 +300,11 @@ class Subscription(threading.Thread):
subscribed = False
type_translation = {
'workspace': 'get_workspaces',
'output': 'get_outputs'
'output': 'get_outputs',
'mode': 'get_binding_modes',
'window': 'get_tree',
'barconfig_update': 'get_bar_config',
'binding': 'get_binding_state',
}

def __init__(self, callback, event_type, event=None, event_socket=None,
Expand Down Expand Up @@ -322,7 +335,7 @@ def run(self):
"""
try:
self.listen()
except socket.error:
except OSError:
self.close()

def listen(self):
Expand All @@ -339,8 +352,11 @@ def listen(self):
if not event: # skip an iteration if event is None
continue
if not self.event or ('change' in event and event['change'] == self.event):
msg_type = self.type_translation[self.event_type]
data = self.data_socket.get(msg_type)
msg_type = self.type_translation.get(self.event_type)
if msg_type:
data = self.data_socket.get(msg_type)
else:
data = None
else:
data = None
self.callback(event, data, self)
Expand Down Expand Up @@ -512,7 +528,7 @@ def filter(tree=None, function=None, **conditions):
try:
if function(tree):
return [tree]
except (KeyError, IndexError):
except (KeyError, IndexError, TypeError):
pass
else:
for key, value in conditions.items():
Expand All @@ -528,7 +544,7 @@ def filter(tree=None, function=None, **conditions):
return matches


class i3(ModuleType):
class i3(types.ModuleType):
"""
i3.py is a Python module for communicating with the i3 window manager.
"""
Expand Down
Loading