polybar-status-indicators

Freedesktop's StatusNotifierHost for polybar
git clone https://git.ce9e.org/polybar-status-indicators.git

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 ![icons](screenshots/icons.png)
   -1     8 ![rofi showing a NetworkManager menu](screenshots/menu.png)
   -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])

diff --git a/screenshots/icons.png b/screenshots/icons.png

Binary files differ.

diff --git a/screenshots/menu.png b/screenshots/menu.png

Binary files differ.