Skip to content

Commit 26b2972

Browse files
author
Tony Crisci
authored
Merge pull request #65 from tarruda/allow-asyncio-polling-event-socket
Allow usage from external event loops.
2 parents 50a6314 + a955a37 commit 26b2972

2 files changed

Lines changed: 199 additions & 39 deletions

File tree

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
#!/usr/bin/env python3
2+
# This module is an example of how to use i3ipc with asyncio event loop. It
3+
# implements an i3status wrapper that handles a special keybinding to switch
4+
# keyboard layout, while also displaying current layout in i3bar.
5+
#
6+
# The keyboard layout switcher can be activated by adding something like this
7+
# to i3 config:
8+
#
9+
# bindsym KEYS nop switch_layout
10+
11+
import asyncio
12+
import collections
13+
import json
14+
import subprocess
15+
import sys
16+
import tempfile
17+
18+
import i3ipc
19+
20+
configure_i3_status = False
21+
try:
22+
# Unfortunately i3status does not have a simple way to set the
23+
# output_format outside of its configuration file. If not set, it will
24+
# guess the output format in a very hacky way by looking at the parent
25+
# process name which is horrible for embedders. So, we try to "fool" it
26+
# into using i3bar output format by changing the process title with
27+
# setproctitle module (install with pip3 install --user setproctitle).
28+
29+
# Of course, this is not needed if output_format is set explicitly in the
30+
# config file. This is only done for demonstration purposes.
31+
import setproctitle
32+
setproctitle.setproctitle('i3bar')
33+
except ImportError as e:
34+
# Configure i3status by explicitly setting "i3bar" as output_format
35+
configure_i3_status = True
36+
37+
I3STATUS_CFG = '''
38+
general {
39+
output_format = "i3bar"
40+
colors = true
41+
interval = 5
42+
}
43+
44+
order += "disk /"
45+
order += "load"
46+
order += "tztime local"
47+
48+
tztime local {
49+
format = "%Y-%m-%d %H:%M:%S"
50+
}
51+
52+
load {
53+
format = "%1min"
54+
}
55+
56+
disk "/" {
57+
format = "%avail"
58+
}
59+
'''
60+
61+
62+
class Status(object):
63+
def __init__(self):
64+
self.current_status = collections.OrderedDict()
65+
# the first write does not contain a leading newline since it
66+
# represents the first item in a json array.
67+
self.first_write = True
68+
self.layouts = ['us', 'us intl']
69+
self.current_layout = -1
70+
self.command_handlers = {
71+
'switch_layout': lambda: self.switch_layout()
72+
}
73+
# perform a switch now, which will force the keyboard layout to be
74+
# shown before other data
75+
self.switch_layout()
76+
77+
def switch_layout(self):
78+
self.current_layout = (self.current_layout + 1) % len(self.layouts)
79+
new_layout = self.layouts[self.current_layout]
80+
subprocess.call('setxkbmap {}'.format(new_layout), shell=True)
81+
self.update([{
82+
'name': 'keyboard_layout',
83+
'markup': 'none',
84+
'full_text': new_layout
85+
}])
86+
87+
def dispatch_command(self, command):
88+
c = command.split(' ')
89+
if (len(c) < 2 or c[0] != 'nop' or c[1] not in self.command_handlers):
90+
return
91+
self.command_handlers[c[1]]()
92+
self.repaint()
93+
94+
def merge(self, status_update):
95+
for item in status_update:
96+
self.current_status[item['name']] = item
97+
98+
def update(self, new_status):
99+
self.merge(new_status)
100+
101+
def repaint(self):
102+
template = '{}' if self.first_write else ',{}'
103+
self.first_write = False
104+
sys.stdout.write(template.format(
105+
json.dumps([item for item in self.current_status.values()
106+
if item], separators=(',', ':'))))
107+
sys.stdout.write('\n')
108+
sys.stdout.flush()
109+
110+
@asyncio.coroutine
111+
def i3status_reader(self):
112+
def handle_i3status_payload(line):
113+
self.update(json.loads(line))
114+
self.repaint()
115+
116+
if configure_i3_status:
117+
# use a custom i3 status configuration to ensure we get json output
118+
cfg_file = tempfile.NamedTemporaryFile(mode='w+b')
119+
cfg_file.write(I3STATUS_CFG.encode('utf8'))
120+
cfg_file.flush()
121+
create = asyncio.create_subprocess_exec(
122+
'i3status', '-c', cfg_file.name,
123+
stdout=asyncio.subprocess.PIPE)
124+
else:
125+
create = asyncio.create_subprocess_exec(
126+
'i3status', stdout=asyncio.subprocess.PIPE)
127+
i3status = yield from create
128+
# forward first line, version information
129+
sys.stdout.write(
130+
(yield from i3status.stdout.readline()).decode('utf8'))
131+
# forward second line, an opening list bracket (no idea why this
132+
# exists)
133+
sys.stdout.write(
134+
(yield from i3status.stdout.readline()).decode('utf8'))
135+
# third line is a json payload
136+
handle_i3status_payload(
137+
(yield from i3status.stdout.readline()).decode('utf8'))
138+
while True:
139+
# all subsequent lines are json payload with a leading comma
140+
handle_i3status_payload(
141+
(yield from i3status.stdout.readline()).decode('utf8')[1:])
142+
143+
144+
status = Status()
145+
146+
i3 = i3ipc.Connection()
147+
i3.on('binding::run', lambda i3, e: status.dispatch_command(e.binding.command))
148+
i3.event_socket_setup()
149+
loop = asyncio.get_event_loop()
150+
loop.add_reader(i3.sub_socket, lambda: i3.event_socket_poll())
151+
loop.run_until_complete(status.i3status_reader())

i3ipc/i3ipc.py

Lines changed: 48 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -571,56 +571,65 @@ def on(self, detailed_event, handler):
571571

572572
self._pubsub.subscribe(detailed_event, handler)
573573

574-
def main(self):
574+
def event_socket_setup(self):
575575
self.sub_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
576576
self.sub_socket.connect(self.socket_path)
577577

578578
self.subscribe(self.subscriptions)
579579

580-
while True:
581-
if self.sub_socket is None:
582-
break
580+
def event_socket_teardown(self):
581+
if self.sub_socket:
582+
self.sub_socket.shutdown(socket.SHUT_WR)
583+
self.sub_socket = None
583584

584-
data, msg_type = self._ipc_recv(self.sub_socket)
585+
def event_socket_poll(self):
586+
if self.sub_socket is None:
587+
return True
585588

586-
if len(data) == 0:
587-
# EOF
588-
self._pubsub.emit('ipc_shutdown', None)
589-
break
589+
data, msg_type = self._ipc_recv(self.sub_socket)
590590

591-
data = json.loads(data)
592-
msg_type = 1 << (msg_type & 0x7f)
593-
event_name = ''
594-
event = None
595-
596-
if msg_type == Event.WORKSPACE:
597-
event_name = 'workspace'
598-
event = WorkspaceEvent(data, self)
599-
elif msg_type == Event.OUTPUT:
600-
event_name = 'output'
601-
event = GenericEvent(data)
602-
elif msg_type == Event.MODE:
603-
event_name = 'mode'
604-
event = GenericEvent(data)
605-
elif msg_type == Event.WINDOW:
606-
event_name = 'window'
607-
event = WindowEvent(data, self)
608-
elif msg_type == Event.BARCONFIG_UPDATE:
609-
event_name = 'barconfig_update'
610-
event = BarconfigUpdateEvent(data)
611-
elif msg_type == Event.BINDING:
612-
event_name = 'binding'
613-
event = BindingEvent(data)
614-
else:
615-
# we have not implemented this event
616-
continue
591+
if len(data) == 0:
592+
# EOF
593+
self._pubsub.emit('ipc_shutdown', None)
594+
return True
595+
596+
data = json.loads(data)
597+
msg_type = 1 << (msg_type & 0x7f)
598+
event_name = ''
599+
event = None
600+
601+
if msg_type == Event.WORKSPACE:
602+
event_name = 'workspace'
603+
event = WorkspaceEvent(data, self)
604+
elif msg_type == Event.OUTPUT:
605+
event_name = 'output'
606+
event = GenericEvent(data)
607+
elif msg_type == Event.MODE:
608+
event_name = 'mode'
609+
event = GenericEvent(data)
610+
elif msg_type == Event.WINDOW:
611+
event_name = 'window'
612+
event = WindowEvent(data, self)
613+
elif msg_type == Event.BARCONFIG_UPDATE:
614+
event_name = 'barconfig_update'
615+
event = BarconfigUpdateEvent(data)
616+
elif msg_type == Event.BINDING:
617+
event_name = 'binding'
618+
event = BindingEvent(data)
619+
else:
620+
# we have not implemented this event
621+
return
622+
623+
self._pubsub.emit(event_name, event)
624+
625+
def main(self):
626+
self.event_socket_setup()
617627

618-
self._pubsub.emit(event_name, event)
628+
while not self.event_socket_poll():
629+
pass
619630

620631
def main_quit(self):
621-
if self.sub_socket:
622-
self.sub_socket.shutdown(socket.SHUT_WR)
623-
self.sub_socket = None
632+
self.event_socket_teardown()
624633

625634

626635
class Rect(object):

0 commit comments

Comments
 (0)