mirror of
https://github.com/vincentbernat/i3wm-configuration.git
synced 2025-06-24 18:58:33 +02:00
i3-companion: replace network module by custom IPC for Network Manager
This commit is contained in:
parent
b3c386b778
commit
7df5011824
3 changed files with 176 additions and 33 deletions
189
bin/i3-companion
189
bin/i3-companion
|
@ -3,16 +3,20 @@
|
||||||
"""Personal i3 companion."""
|
"""Personal i3 companion."""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import collections
|
||||||
|
import errno
|
||||||
|
import fcntl
|
||||||
|
import functools
|
||||||
|
import glob
|
||||||
|
import html
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
import sys
|
import os
|
||||||
import re
|
import re
|
||||||
import asyncio
|
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import html
|
import sys
|
||||||
import functools
|
|
||||||
import collections
|
|
||||||
|
|
||||||
from i3ipc.aio import Connection
|
from i3ipc.aio import Connection
|
||||||
from i3ipc import Event
|
from i3ipc import Event
|
||||||
|
@ -25,6 +29,15 @@ logger = logging.getLogger("i3-companion")
|
||||||
DBusSignal = collections.namedtuple(
|
DBusSignal = collections.namedtuple(
|
||||||
"DBusSignal", ["path", "interface", "member", "signature"]
|
"DBusSignal", ["path", "interface", "member", "signature"]
|
||||||
)
|
)
|
||||||
|
StartEvent = object()
|
||||||
|
|
||||||
|
|
||||||
|
NM_ACTIVE_CONNECTION_STATE_ACTIVATED = 2
|
||||||
|
NM_DEVICE_TYPE_ETHERNET = 1
|
||||||
|
NM_DEVICE_TYPE_WIFI = 2
|
||||||
|
NM_DEVICE_TYPE_MODEM = 8
|
||||||
|
NM_DEVICE_STATE_UNMANAGED = 10
|
||||||
|
NM_DEVICE_STATE_ACTIVATED = 100
|
||||||
|
|
||||||
|
|
||||||
def on(*events):
|
def on(*events):
|
||||||
|
@ -46,7 +59,9 @@ async def notify(i3, **kwargs):
|
||||||
"""Send a notification with notify-send."""
|
"""Send a notification with notify-send."""
|
||||||
peer = i3.session_bus["org.freedesktop.Notifications"]
|
peer = i3.session_bus["org.freedesktop.Notifications"]
|
||||||
peer = peer["/org/freedesktop/Notifications"]
|
peer = peer["/org/freedesktop/Notifications"]
|
||||||
interface = await peer.get_async_interface("org.freedesktop.Notifications")
|
notifications = await peer.get_async_interface(
|
||||||
|
"org.freedesktop.Notifications"
|
||||||
|
)
|
||||||
parameters = dict(
|
parameters = dict(
|
||||||
app_name=logger.name,
|
app_name=logger.name,
|
||||||
replaces_id=0,
|
replaces_id=0,
|
||||||
|
@ -57,7 +72,7 @@ async def notify(i3, **kwargs):
|
||||||
expire_timeout=5000,
|
expire_timeout=5000,
|
||||||
)
|
)
|
||||||
parameters.update(kwargs)
|
parameters.update(kwargs)
|
||||||
return await interface.Notify(**parameters)
|
return await notifications.Notify(**parameters)
|
||||||
|
|
||||||
|
|
||||||
# See https://fontawesome.com/v5.15/icons
|
# See https://fontawesome.com/v5.15/icons
|
||||||
|
@ -355,8 +370,11 @@ async def output_update(i3, event):
|
||||||
running.cancel()
|
running.cancel()
|
||||||
output_update.running = None
|
output_update.running = None
|
||||||
|
|
||||||
def output_update_now():
|
async def output_update_now():
|
||||||
"""Execute actions to react to XRandR change."""
|
try:
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return
|
||||||
output_update.running = None
|
output_update.running = None
|
||||||
|
|
||||||
logger.info("XRandR change detected")
|
logger.info("XRandR change detected")
|
||||||
|
@ -370,9 +388,7 @@ async def output_update(i3, event):
|
||||||
logger.warning(f"{cmd} exited with {proc.returncode}")
|
logger.warning(f"{cmd} exited with {proc.returncode}")
|
||||||
|
|
||||||
logger.debug("schedule XRandR change")
|
logger.debug("schedule XRandR change")
|
||||||
output_update.running = asyncio.get_event_loop().call_later(
|
output_update.running = asyncio.create_task(output_update_now())
|
||||||
2, output_update_now
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@on(
|
@on(
|
||||||
|
@ -384,22 +400,22 @@ async def output_update(i3, event):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
async def network_manager_notifications(i3, event, path, state, reason):
|
async def network_manager_notifications(i3, event, path, state, reason):
|
||||||
logger.debug("from %s state: %d, reason: %d", path, state, reason)
|
"""Display notifications related to Network Manager state."""
|
||||||
NM_ACTIVE_CONNECTION_STATE_ACTIVATED = 2
|
logger.debug(f"from {path} state: {state}, reason: {reason}")
|
||||||
if state not in {NM_ACTIVE_CONNECTION_STATE_ACTIVATED}:
|
if state not in {NM_ACTIVE_CONNECTION_STATE_ACTIVATED}:
|
||||||
# We cannot get proper state unless the connection is
|
# We cannot get proper state unless the connection is
|
||||||
# activated, unless we maintain state.
|
# activated, unless we maintain state.
|
||||||
return
|
return
|
||||||
peer = i3.system_bus["org.freedesktop.NetworkManager"][path]
|
peer = i3.system_bus["org.freedesktop.NetworkManager"][path]
|
||||||
try:
|
try:
|
||||||
interface = await peer.get_async_interface(
|
nmca = await peer.get_async_interface(
|
||||||
"org.freedesktop.NetworkManager.Connection.Active"
|
"org.freedesktop.NetworkManager.Connection.Active"
|
||||||
)
|
)
|
||||||
except dbussy.DBusError:
|
except dbussy.DBusError:
|
||||||
logger.info("interface %s has vanished", path)
|
logger.info(f"interface {path} has vanished")
|
||||||
return
|
return
|
||||||
kind = await interface.Type
|
kind = await nmca.Type
|
||||||
id = await interface.Id
|
id = await nmca.Id
|
||||||
if kind == "vpn":
|
if kind == "vpn":
|
||||||
await notify(
|
await notify(
|
||||||
i3, app_icon="network-vpn", summary=f"{id}", body="VPN connected!"
|
i3, app_icon="network-vpn", summary=f"{id}", body="VPN connected!"
|
||||||
|
@ -420,6 +436,135 @@ async def network_manager_notifications(i3, event, path, state, reason):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@on(
|
||||||
|
StartEvent,
|
||||||
|
DBusSignal(
|
||||||
|
path="/",
|
||||||
|
interface="org.freedesktop.NetworkManager.Connection.Active",
|
||||||
|
member="StateChanged",
|
||||||
|
signature="uu",
|
||||||
|
),
|
||||||
|
DBusSignal(
|
||||||
|
path="/",
|
||||||
|
interface="org.freedesktop.NetworkManager.AccessPoint",
|
||||||
|
member="PropertiesChanged",
|
||||||
|
signature="a{sv}",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def network_manager_status(i3, event, *args):
|
||||||
|
"""Compute network manager status."""
|
||||||
|
if isinstance(event, DBusSignal) and event.interface == "org.freedesktop.NetworkManager.AccessPoint":
|
||||||
|
path, props = args
|
||||||
|
if getattr(network_manager_status, "active_ap", None) != path:
|
||||||
|
return
|
||||||
|
if not "Strength" in props:
|
||||||
|
return
|
||||||
|
|
||||||
|
running = getattr(network_manager_status, "running", None)
|
||||||
|
if running is not None:
|
||||||
|
running.cancel()
|
||||||
|
network_manager_status.running = None
|
||||||
|
|
||||||
|
async def network_manager_status_now(sleep=1):
|
||||||
|
try:
|
||||||
|
await asyncio.sleep(sleep)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return
|
||||||
|
network_manager_status.running = None
|
||||||
|
try:
|
||||||
|
await network_manager_status__now()
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("while updating network status: %s", e)
|
||||||
|
if network_manager_status.running is None:
|
||||||
|
network_manager_status.running = asyncio.create_task(
|
||||||
|
network_manager_status_now(5)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def network_manager_status__now():
|
||||||
|
status = []
|
||||||
|
|
||||||
|
# Build status from devices
|
||||||
|
bus = i3.system_bus["org.freedesktop.NetworkManager"]
|
||||||
|
nm = await bus["/org/freedesktop/NetworkManager"].get_async_interface(
|
||||||
|
"org.freedesktop.NetworkManager"
|
||||||
|
)
|
||||||
|
devices = await nm.AllDevices
|
||||||
|
for device in devices:
|
||||||
|
nmd = await bus[device].get_async_interface(
|
||||||
|
"org.freedesktop.NetworkManager.Device"
|
||||||
|
)
|
||||||
|
kind = await nmd.DeviceType
|
||||||
|
state = await nmd.State
|
||||||
|
if state == NM_DEVICE_STATE_UNMANAGED:
|
||||||
|
continue
|
||||||
|
if kind == NM_DEVICE_TYPE_WIFI:
|
||||||
|
if state != NM_DEVICE_STATE_ACTIVATED:
|
||||||
|
status.append("")
|
||||||
|
continue
|
||||||
|
nmw = await bus[device].get_async_interface(
|
||||||
|
"org.freedesktop.NetworkManager.Device.Wireless"
|
||||||
|
)
|
||||||
|
ap = await nmw.ActiveAccessPoint
|
||||||
|
if not ap:
|
||||||
|
status.append("")
|
||||||
|
continue
|
||||||
|
network_manager_status.active_ap = ap
|
||||||
|
nmap = await bus[ap].get_async_interface(
|
||||||
|
"org.freedesktop.NetworkManager.AccessPoint"
|
||||||
|
)
|
||||||
|
name = await nmap.Ssid
|
||||||
|
strength = int(await nmap.Strength)
|
||||||
|
status.append(""[strength // 34])
|
||||||
|
status.append(bytes(name).decode("utf-8"))
|
||||||
|
elif (
|
||||||
|
kind == NM_DEVICE_TYPE_ETHERNET
|
||||||
|
and state == NM_DEVICE_STATE_ACTIVATED
|
||||||
|
):
|
||||||
|
status.append("")
|
||||||
|
|
||||||
|
# Build status for VPN connection
|
||||||
|
connections = await nm.ActiveConnections
|
||||||
|
for conn in connections:
|
||||||
|
nma = await bus[conn].get_async_interface(
|
||||||
|
"org.freedesktop.NetworkManager.Connection.Active"
|
||||||
|
)
|
||||||
|
vpn = await nma.Vpn
|
||||||
|
if vpn:
|
||||||
|
state = await nma.State
|
||||||
|
if state == NM_ACTIVE_CONNECTION_STATE_ACTIVATED:
|
||||||
|
status.append("︁")
|
||||||
|
status.append(await nma.Id)
|
||||||
|
|
||||||
|
# Final status line
|
||||||
|
status = " ".join(status).replace("%", "%%")
|
||||||
|
last = getattr(network_manager_status, "last", None)
|
||||||
|
|
||||||
|
if status != last:
|
||||||
|
logger.info(f"network status: {status}")
|
||||||
|
network_manager_status.last = status
|
||||||
|
|
||||||
|
# Update cache file (for when polybar restarts)
|
||||||
|
with open(f"{os.getenv('XDG_RUNTIME_DIR')}/i3/network.txt", "w") as out:
|
||||||
|
out.write(status)
|
||||||
|
|
||||||
|
# Send it to module/network
|
||||||
|
for name in glob.glob("/tmp/polybar_mqueue.*"):
|
||||||
|
try:
|
||||||
|
with open(name, "w") as out:
|
||||||
|
fd = out.fileno()
|
||||||
|
old_flags = fcntl.fcntl(fd, fcntl.F_GETFL)
|
||||||
|
fcntl.fcntl(fd, fcntl.F_SETFL, old_flags | os.O_NONBLOCK)
|
||||||
|
cmd = f"action:#network.send.{status}"
|
||||||
|
out.write(cmd)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno == errno.ENXIO:
|
||||||
|
pass
|
||||||
|
|
||||||
|
network_manager_status.running = asyncio.create_task(
|
||||||
|
network_manager_status_now()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def main(options):
|
async def main(options):
|
||||||
i3 = await Connection().connect()
|
i3 = await Connection().connect()
|
||||||
i3.session_bus = await ravel.session_bus_async()
|
i3.session_bus = await ravel.session_bus_async()
|
||||||
|
@ -478,6 +623,12 @@ async def main(options):
|
||||||
func=wrapping(fn, event),
|
func=wrapping(fn, event),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Run events that should run on start
|
||||||
|
for fn, events in on.functions.items():
|
||||||
|
for event in events:
|
||||||
|
if event is StartEvent:
|
||||||
|
asyncio.create_task(fn(i3, event))
|
||||||
|
|
||||||
await i3.main()
|
await i3.main()
|
||||||
|
|
||||||
|
|
||||||
|
@ -507,7 +658,7 @@ if __name__ == "__main__":
|
||||||
root.addHandler(journal.JournalHandler(SYSLOG_IDENTIFIER=logger.name))
|
root.addHandler(journal.JournalHandler(SYSLOG_IDENTIFIER=logger.name))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
asyncio.get_event_loop().run_until_complete(main(options))
|
asyncio.run(main(options))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("%s", e)
|
logger.exception("%s", e)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
export LAN=$(nmcli -t device | awk -F: '($2 == "ethernet") {print $1; exit}')
|
|
||||||
export WLAN=$(nmcli -t device | awk -F: '($2 == "wifi") {print $1; exit}')
|
|
||||||
export DPI=$(xrdb -query | sed -nE 's/^Xft\.dpi:\s*//p')
|
export DPI=$(xrdb -query | sed -nE 's/^Xft\.dpi:\s*//p')
|
||||||
export HEIGHT=$((20 * DPI / 96))
|
export HEIGHT=$((20 * DPI / 96))
|
||||||
|
|
||||||
|
|
|
@ -31,11 +31,11 @@ modules-center = date
|
||||||
|
|
||||||
[bar/alone]
|
[bar/alone]
|
||||||
inherit = bar/common
|
inherit = bar/common
|
||||||
modules-right = cpu memory brightness battery ethernet wlan disk pulseaudio
|
modules-right = cpu memory brightness battery network disk pulseaudio
|
||||||
|
|
||||||
[bar/primary]
|
[bar/primary]
|
||||||
inherit = bar/common
|
inherit = bar/common
|
||||||
modules-right = cpu memory brightness battery ethernet wlan disk pulseaudio
|
modules-right = cpu memory brightness battery network disk pulseaudio
|
||||||
|
|
||||||
[bar/secondary]
|
[bar/secondary]
|
||||||
inherit = bar/common
|
inherit = bar/common
|
||||||
|
@ -98,16 +98,10 @@ type = internal/memory
|
||||||
interval = 5
|
interval = 5
|
||||||
label = %percentage_used: 2%%
|
label = %percentage_used: 2%%
|
||||||
|
|
||||||
[module/wlan]
|
[module/network]
|
||||||
type = internal/network
|
type = custom/ipc
|
||||||
interface = ${env:WLAN:}
|
hook-0 = cat $XDG_RUNTIME_DIR/i3/network.txt
|
||||||
interval = 5
|
initial = 1
|
||||||
format-connected = <ramp-signal> <label-connected>
|
|
||||||
label-connected = %essid%
|
|
||||||
format-disconnected =
|
|
||||||
ramp-signal-0 =
|
|
||||||
ramp-signal-1 =
|
|
||||||
ramp-signal-2 =
|
|
||||||
|
|
||||||
[module/ethernet]
|
[module/ethernet]
|
||||||
type = internal/network
|
type = internal/network
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue