|
| 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()) |
0 commit comments