- commit
- 8ff1032e62f20eda1b84be0e9b2700a778a9a792
- parent
- f2c2a96f337b3454eea2652b2c2b1190f8e4a76a
- Author
- Tobias Bengfort <tobias.bengfort@posteo.de>
- Date
- 2022-02-17 08:04
add files see https://github.com/xi/polybar-scripts/tree/master/polybar-scripts/status-indicators-tail
Diffstat
| A | README.md | 29 | +++++++++++++++++++++++++++++ |
| A | host.py | 167 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | menu.py | 110 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | screenshots/icons.png | 0 | |
| A | screenshots/menu.png | 0 |
5 files changed, 306 insertions, 0 deletions
diff --git a/README.md b/README.md
@@ -0,0 +1,29 @@ -1 1 An implementation of the freedesktop [StatusNotifierItem][0] specification (the -1 2 successor of appindicators and systray) for polybar. -1 3 -1 4 This allows you to use many existing status indicators, e.g. for NetworkManager -1 5 or Steam. Menus are supported via rofi. -1 6 -1 7  -1 8  -1 9 -1 10 ## Dependencies -1 11 -1 12 - python3-gi -1 13 - rofi (or a similar dmenu-like tool) -1 14 -1 15 ## Configuration -1 16 -1 17 - In host.py, adapt `render()` to use your icons and colors -1 18 - In menu.py, adapt `DMENU_CMD` to your preferred dmenu-like tool -1 19 -1 20 ## Module -1 21 -1 22 ```ini -1 23 [module/indicators] -1 24 type = custom/script -1 25 exec = python3 -u ~/polybar-status-indicators/host.py -1 26 tail = true -1 27 ``` -1 28 -1 29 [0]: https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/
diff --git a/host.py b/host.py
@@ -0,0 +1,167 @@
-1 1 import os
-1 2 import sys
-1 3
-1 4 import gi
-1 5
-1 6 gi.require_version('Gtk', '3.0')
-1 7
-1 8 from gi.repository import Gio # noqa
-1 9 from gi.repository import GLib # noqa
-1 10
-1 11 MENU_PATH = os.path.join(os.path.dirname(__file__), 'menu.py')
-1 12
-1 13 NODE_INFO = Gio.DBusNodeInfo.new_for_xml("""
-1 14 <?xml version="1.0" encoding="UTF-8"?>
-1 15 <node>
-1 16 <interface name="org.kde.StatusNotifierWatcher">
-1 17 <method name="RegisterStatusNotifierItem">
-1 18 <arg type="s" direction="in"/>
-1 19 </method>
-1 20 <property name="RegisteredStatusNotifierItems" type="as" access="read">
-1 21 </property>
-1 22 <property name="IsStatusNotifierHostRegistered" type="b" access="read">
-1 23 </property>
-1 24 </interface>
-1 25 </node>""")
-1 26
-1 27 items = {}
-1 28
-1 29
-1 30 def render():
-1 31 # customize this function to your needs
-1 32 # see https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierItem/
-1 33 # for available fields
-1 34 labels = []
-1 35 for key, item in reversed(items.items()):
-1 36 name, path = key.split('/', 1)
-1 37
-1 38 if item['Status'] == 'Passive':
-1 39 continue
-1 40
-1 41 label = f'[{item["IconName"]}]'
-1 42
-1 43 cmd = (
-1 44 f'busctl --user call \\{name} /{path} '
-1 45 'org.kde.StatusNotifierItem Activate ii 0 0'
-1 46 )
-1 47 menu_cmd = f'python3 {MENU_PATH} \\{name} {item["Menu"]}'
-1 48
-1 49 label = f'%{{A1:{cmd}:}}{label}%{{A}}'
-1 50 label = f'%{{A3:{menu_cmd}:}}{label}%{{A}}'
-1 51
-1 52 labels.append(label)
-1 53
-1 54 print(' '.join(labels))
-1 55
-1 56
-1 57 def get_item_data(conn, sender, path):
-1 58 def callback(conn, red, user_data=None):
-1 59 args = conn.call_finish(red)
-1 60 items[sender + path] = args[0]
-1 61 render()
-1 62
-1 63 conn.call(
-1 64 sender,
-1 65 path,
-1 66 'org.freedesktop.DBus.Properties',
-1 67 'GetAll',
-1 68 GLib.Variant('(s)', ['org.kde.StatusNotifierItem']),
-1 69 GLib.VariantType('(a{sv})'),
-1 70 Gio.DBusCallFlags.NONE,
-1 71 -1,
-1 72 None,
-1 73 callback,
-1 74 None,
-1 75 )
-1 76
-1 77
-1 78 def on_call(
-1 79 conn, sender, path, interface, method, params, invocation, user_data=None
-1 80 ):
-1 81 props = {
-1 82 'RegisteredStatusNotifierItems': GLib.Variant('as', items.keys()),
-1 83 'IsStatusNotifierHostRegistered': GLib.Variant('b', True),
-1 84 }
-1 85
-1 86 if method == 'Get' and params[1] in props:
-1 87 invocation.return_value(GLib.Variant('(v)', [props[params[1]]]))
-1 88 conn.flush()
-1 89 if method == 'GetAll':
-1 90 invocation.return_value(GLib.Variant('(a{sv})', [props]))
-1 91 conn.flush()
-1 92 elif method == 'RegisterStatusNotifierItem':
-1 93 if params[0].startswith('/'):
-1 94 path = params[0]
-1 95 else:
-1 96 path = '/StatusNotifierItem'
-1 97 get_item_data(conn, sender, path)
-1 98 invocation.return_value(None)
-1 99 conn.flush()
-1 100
-1 101
-1 102 def on_signal(
-1 103 conn, sender, path, interface, signal, params, invocation, user_data=None
-1 104 ):
-1 105 if signal == 'NameOwnerChanged':
-1 106 if params[2] != '':
-1 107 return
-1 108 keys = [key for key in items if key.startswith(params[0] + '/')]
-1 109 if not keys:
-1 110 return
-1 111 for key in keys:
-1 112 del items[key]
-1 113 render()
-1 114 elif sender + path in items:
-1 115 get_item_data(conn, sender, path)
-1 116
-1 117
-1 118 def on_bus_acquired(conn, name, user_data=None):
-1 119 for interface in NODE_INFO.interfaces:
-1 120 if interface.name == name:
-1 121 conn.register_object('/StatusNotifierWatcher', interface, on_call)
-1 122
-1 123 def signal_subscribe(interface, signal):
-1 124 conn.signal_subscribe(
-1 125 None, # sender
-1 126 interface,
-1 127 signal,
-1 128 None, # path
-1 129 None,
-1 130 Gio.DBusSignalFlags.NONE,
-1 131 on_signal,
-1 132 None, # user_data
-1 133 )
-1 134
-1 135 signal_subscribe('org.freedesktop.DBus', 'NameOwnerChanged')
-1 136 for signal in [
-1 137 'NewAttentionIcon',
-1 138 'NewIcon',
-1 139 'NewIconThemePath',
-1 140 'NewStatus',
-1 141 'NewTitle',
-1 142 ]:
-1 143 signal_subscribe('org.kde.StatusNotifierItem', signal)
-1 144
-1 145
-1 146 def on_name_lost(conn, name, user_data=None):
-1 147 sys.exit(
-1 148 f'Could not aquire name {name}. '
-1 149 f'Is some other service blocking it?'
-1 150 )
-1 151
-1 152
-1 153 if __name__ == '__main__':
-1 154 owner_id = Gio.bus_own_name(
-1 155 Gio.BusType.SESSION,
-1 156 NODE_INFO.interfaces[0].name,
-1 157 Gio.BusNameOwnerFlags.NONE,
-1 158 on_bus_acquired,
-1 159 None,
-1 160 on_name_lost,
-1 161 )
-1 162
-1 163 try:
-1 164 loop = GLib.MainLoop()
-1 165 loop.run()
-1 166 finally:
-1 167 Gio.bus_unown_name(owner_id)
diff --git a/menu.py b/menu.py
@@ -0,0 +1,110 @@
-1 1 import subprocess
-1 2 import sys
-1 3 import time
-1 4
-1 5 import gi
-1 6
-1 7 gi.require_version('Gtk', '3.0')
-1 8
-1 9 from gi.repository import Gio # noqa
-1 10 from gi.repository import GLib # noqa
-1 11
-1 12 DMENU_CMD = ['rofi', '-dmenu', '-i', '-no-sort']
-1 13
-1 14
-1 15 class Bus:
-1 16 def __init__(self, conn, name, path):
-1 17 self.conn = conn
-1 18 self.name = name
-1 19 self.path = path
-1 20
-1 21 def call_sync(self, interface, method, params, params_type, return_type):
-1 22 return self.conn.call_sync(
-1 23 self.name,
-1 24 self.path,
-1 25 interface,
-1 26 method,
-1 27 GLib.Variant(params_type, params),
-1 28 GLib.VariantType(return_type),
-1 29 Gio.DBusCallFlags.NONE,
-1 30 -1,
-1 31 None,
-1 32 )
-1 33
-1 34 def get_menu_layout(self, *args):
-1 35 return self.call_sync(
-1 36 'com.canonical.dbusmenu',
-1 37 'GetLayout',
-1 38 args,
-1 39 '(iias)',
-1 40 '(u(ia{sv}av))',
-1 41 )
-1 42
-1 43 def menu_event(self, *args):
-1 44 self.call_sync('com.canonical.dbusmenu', 'Event', args, '(isvu)', '()')
-1 45
-1 46
-1 47 def dmenu(_input):
-1 48 p = subprocess.Popen(
-1 49 DMENU_CMD,
-1 50 stdin=subprocess.PIPE,
-1 51 stdout=subprocess.PIPE,
-1 52 encoding='utf-8',
-1 53 )
-1 54 out, _ = p.communicate(_input)
-1 55 return out
-1 56
-1 57
-1 58 def format_toggle_value(props):
-1 59 toggle_type = props.get('toggle-type', '')
-1 60 toggle_value = props.get('toggle-state', -1)
-1 61
-1 62 if toggle_value == 0:
-1 63 s = ' '
-1 64 elif toggle_value == 1:
-1 65 s = 'X'
-1 66 else:
-1 67 s = '~'
-1 68
-1 69 if toggle_type == 'checkmark':
-1 70 return f'[{s}] '
-1 71 elif toggle_type == 'radio':
-1 72 return f'({s}) '
-1 73 else:
-1 74 return ''
-1 75
-1 76
-1 77 def format_menu_item(item, level=1):
-1 78 id, props, children = item
-1 79
-1 80 if not props.get('visible', True):
-1 81 return ''
-1 82 if props.get('type', 'standard') == 'separator':
-1 83 label = '---'
-1 84 else:
-1 85 label = format_toggle_value(props) + props.get('label', '')
-1 86 if not props.get('enabled', True):
-1 87 label = f'({label})'
-1 88
-1 89 indentation = ' ' * level
-1 90 ret = f'{id}{indentation}{label}\n'
-1 91 for child in children:
-1 92 ret += format_menu_item(child, level + 1)
-1 93 return ret
-1 94
-1 95
-1 96 def show_menu(conn, name, path):
-1 97 bus = Bus(conn, name, path)
-1 98 item = bus.get_menu_layout(0, -1, [])[1]
-1 99
-1 100 menu = format_menu_item(item)
-1 101 selected = dmenu(menu)
-1 102
-1 103 if selected:
-1 104 id = int(selected.split()[0])
-1 105 bus.menu_event(id, 'clicked', GLib.Variant('s', ''), time.time())
-1 106
-1 107
-1 108 if __name__ == '__main__':
-1 109 conn = Gio.bus_get_sync(Gio.BusType.SESSION)
-1 110 show_menu(conn, sys.argv[1], sys.argv[2])