dotfiles/.local/bin/networkmanager_dmenu

1130 lines
36 KiB
Plaintext
Raw Permalink Normal View History

2024-04-23 07:34:15 +05:00
#!/usr/bin/env python3
# encoding:utf8
"""NetworkManager command line dmenu script.
To add new connections or enable/disable networking requires policykit
permissions setup per:
https://wiki.archlinux.org/index.php/NetworkManager#Set_up_PolicyKit_permissions
OR running the script as root
Add dmenu options and default terminal if desired to
~/.config/networkmanager-dmenu/config.ini
"""
2024-10-05 07:32:04 +05:00
2024-04-23 07:34:15 +05:00
import configparser
import locale
import os
2024-10-05 07:32:04 +05:00
import pathlib
2024-04-23 07:34:15 +05:00
import shlex
2024-10-05 07:32:04 +05:00
import struct
import subprocess
2024-04-23 07:34:15 +05:00
import sys
import uuid
2024-10-05 07:32:04 +05:00
from os.path import basename, expanduser
from shutil import which
from time import sleep
2024-04-23 07:34:15 +05:00
# pylint: disable=import-error
import gi
2024-10-05 07:32:04 +05:00
gi.require_version("NM", "1.0")
2024-04-23 07:34:15 +05:00
from gi.repository import GLib, NM # noqa pylint: disable=wrong-import-position
# pylint: enable=import-error
ENV = os.environ.copy()
ENC = locale.getpreferredencoding()
CONF = configparser.ConfigParser()
CONF.read(expanduser("~/.config/networkmanager-dmenu/config.ini"))
def cli_args():
2024-10-05 07:32:04 +05:00
"""Don't override dmenu_cmd function arguments with CLI args. Removes -l
2024-04-23 07:34:15 +05:00
and -p if those are passed on the command line.
Exception: if -l is passed and dmenu_command is not defined, assume that the
user wants to switch dmenu to the vertical layout and include -l.
Returns: List of additional CLI arguments
"""
args = sys.argv[1:]
2024-10-05 07:32:04 +05:00
cmd = CONF.get("dmenu", "dmenu_command", fallback=False)
2024-04-23 07:34:15 +05:00
if "-l" in args or "-p" in args:
2024-10-05 07:32:04 +05:00
for nope in ["-l", "-p"] if cmd is not False else ["-p"]:
2024-04-23 07:34:15 +05:00
try:
nope_idx = args.index(nope)
del args[nope_idx]
del args[nope_idx]
except ValueError:
pass
return args
def dmenu_pass(command, color):
"""Check if dmenu passphrase patch is applied and return the correct command
line arg list
Args: command - string
color - obscure color string
Returns: list or None
"""
2024-10-05 07:32:04 +05:00
if command != "dmenu":
2024-04-23 07:34:15 +05:00
return None
try:
# Check for dmenu password patch
2024-10-05 07:32:04 +05:00
dm_patch = (
b"P"
in subprocess.run(["dmenu", "-h"], capture_output=True, check=False).stderr
)
2024-04-23 07:34:15 +05:00
except FileNotFoundError:
dm_patch = False
return ["-P"] if dm_patch else ["-nb", color, "-nf", color]
def dmenu_cmd(num_lines, prompt="Networks", active_lines=None):
"""Parse config.ini for menu options
Args: args - num_lines: number of lines to display
prompt: prompt to show
active_lines: list of line numbers to tag as active
Returns: command invocation (as a list of strings) for example
["dmenu", "-l", "<num_lines>", "-p", "<prompt>", "-i"]
"""
# Create command string
2024-10-05 07:32:04 +05:00
commands = {
"dmenu": ["-p", str(prompt)],
"rofi": ["-dmenu", "-p", str(prompt)],
"bemenu": ["-p", str(prompt)],
"wofi": ["-p", str(prompt)],
"fuzzel": ["-p", str(prompt), "--log-level", "none"],
}
command = shlex.split(CONF.get("dmenu", "dmenu_command", fallback="dmenu"))
2024-04-23 07:34:15 +05:00
cmd_base = basename(command[0])
command.extend(cli_args())
command.extend(commands.get(cmd_base, []))
2024-10-05 07:32:04 +05:00
# Highlighting
highlight = CONF.getboolean("dmenu", "highlight", fallback=False)
if highlight is True:
# Rofi
if cmd_base == "rofi" and active_lines:
command.extend(["-a", ",".join([str(num) for num in active_lines])])
# Wofi
if cmd_base == "wofi" and active_lines:
# add '-q' to prevent tag name and properties of pango markup from searchable
command.extend(["-m", "-q"])
2024-04-23 07:34:15 +05:00
# Passphrase prompts
2024-10-05 07:32:04 +05:00
obscure = CONF.getboolean("dmenu_passphrase", "obscure", fallback=False)
2024-04-23 07:34:15 +05:00
if prompt == "Passphrase" and obscure is True:
2024-10-05 07:32:04 +05:00
obscure_color = CONF.get(
"dmenu_passphrase", "obscure_color", fallback="#222222"
)
pass_prompts = {
"dmenu": dmenu_pass(cmd_base, obscure_color),
"rofi": ["-password"],
"bemenu": ["-x"],
"wofi": ["-P"],
"fuzzel": ["--password"],
}
2024-04-23 07:34:15 +05:00
command.extend(pass_prompts.get(cmd_base, []))
return command
def choose_adapter(client):
2024-10-05 07:32:04 +05:00
"""If there is more than one wifi adapter installed, ask which one to use"""
2024-04-23 07:34:15 +05:00
devices = client.get_devices()
devices = [i for i in devices if i.get_device_type() == NM.DeviceType.WIFI]
if not devices:
return None
if len(devices) == 1:
return devices[0]
device_names = "\n".join([d.get_iface() for d in devices])
2024-10-05 07:32:04 +05:00
sel = subprocess.run(
dmenu_cmd(len(devices), "CHOOSE ADAPTER:"),
capture_output=True,
check=False,
env=ENV,
input=device_names,
encoding=ENC,
).stdout
2024-04-23 07:34:15 +05:00
if not sel.strip():
sys.exit()
devices = [i for i in devices if i.get_iface() == sel.strip()]
if len(devices) != 1:
raise ValueError(f"Selection was ambiguous: '{str(sel.strip())}'")
return devices[0]
def is_installed(cmd):
"""Check if a utility is installed"""
return which(cmd) is not None
2024-10-05 07:32:04 +05:00
def is_running(cmd):
try:
subprocess.check_output(["pidof", cmd])
except subprocess.CalledProcessError:
return False
else:
return True
2024-04-23 07:34:15 +05:00
def bluetooth_get_enabled():
2024-10-05 07:32:04 +05:00
"""Check if bluetooth is enabled. Try bluetoothctl first, then rfkill.
2024-04-23 07:34:15 +05:00
Returns None if no bluetooth device was found.
"""
2024-10-05 07:32:04 +05:00
if is_installed("bluetoothctl") and is_running("bluetoothd"):
# Times out in 2 seconds, otherwise bluetoothctl will hang if bluetooth
# service isn't running.
try:
res = subprocess.run(
["bluetoothctl", "show"], timeout=2, capture_output=True, text=True
)
return "Powered: yes" in res.stdout
except subprocess.TimeoutExpired:
pass
2024-04-23 07:34:15 +05:00
# See https://www.kernel.org/doc/Documentation/ABI/stable/sysfs-class-rfkill
2024-10-05 07:32:04 +05:00
for path in pathlib.Path("/sys/class/rfkill/").glob("rfkill*"):
if (path / "type").read_text().strip() == "bluetooth":
return (path / "soft").read_text().strip() == "0"
2024-04-23 07:34:15 +05:00
return None
def create_other_actions(client):
2024-10-05 07:32:04 +05:00
"""Return list of other actions that can be taken"""
2024-04-23 07:34:15 +05:00
networking_enabled = client.networking_get_enabled()
networking_action = "Disable" if networking_enabled else "Enable"
wifi_enabled = client.wireless_get_enabled()
wifi_action = "Disable" if wifi_enabled else "Enable"
bluetooth_enabled = bluetooth_get_enabled()
bluetooth_action = "Disable" if bluetooth_enabled else "Enable"
2024-10-05 07:32:04 +05:00
actions = [
Action(f"{wifi_action} Wifi", toggle_wifi, not wifi_enabled),
Action(
f"{networking_action} Networking", toggle_networking, not networking_enabled
),
]
2024-04-23 07:34:15 +05:00
if bluetooth_enabled is not None:
2024-10-05 07:32:04 +05:00
actions.append(
Action(
f"{bluetooth_action} Bluetooth", toggle_bluetooth, not bluetooth_enabled
)
)
actions += [
Action("Launch Connection Manager", launch_connection_editor),
Action("Delete a Connection", delete_connection),
]
2024-04-23 07:34:15 +05:00
if wifi_enabled:
actions.append(Action("Rescan Wifi Networks", rescan_wifi))
return actions
def rescan_wifi():
"""
Rescan Wifi Access Points
"""
2024-10-05 07:32:04 +05:00
delay = CONF.getint("nmdm", "rescan_delay", fallback=5)
2024-04-23 07:34:15 +05:00
for dev in CLIENT.get_devices():
if gi.repository.NM.DeviceWifi == type(dev):
try:
dev.request_scan_async(None, rescan_cb, None)
LOOP.run()
sleep(delay)
notify("Wifi scan complete")
main()
except gi.repository.GLib.Error as err:
# Too frequent rescan error
notify("Wifi rescan failed", urgency="critical")
if not err.code == 6: # pylint: disable=no-member
raise err
def rescan_cb(dev, res, data):
2024-10-05 07:32:04 +05:00
"""Callback for rescan_wifi. Just for notifications"""
2024-04-23 07:34:15 +05:00
if dev.request_scan_finish(res) is True:
notify("Wifi scan running...")
else:
notify("Wifi scan failed", urgency="critical")
LOOP.quit()
def ssid_to_utf8(nm_ap):
2024-10-05 07:32:04 +05:00
"""Convert binary ssid to utf-8"""
2024-04-23 07:34:15 +05:00
ssid = nm_ap.get_ssid()
if not ssid:
return ""
ret = NM.utils_ssid_to_utf8(ssid.get_data())
return ret
def prompt_saved(saved_cons):
"""Prompt for a saved connection."""
actions = create_saved_actions(saved_cons)
sel = get_selection(actions)
sel()
def ap_security(nm_ap):
2024-10-05 07:32:04 +05:00
"""Parse the security flags to return a string with 'WPA2', etc."""
2024-04-23 07:34:15 +05:00
flags = nm_ap.get_flags()
wpa_flags = nm_ap.get_wpa_flags()
rsn_flags = nm_ap.get_rsn_flags()
sec_str = ""
2024-10-05 07:32:04 +05:00
if (
(flags & getattr(NM, "80211ApFlags").PRIVACY)
and (wpa_flags == 0)
and (rsn_flags == 0)
):
2024-04-23 07:34:15 +05:00
sec_str = " WEP"
if wpa_flags:
sec_str = " WPA1"
2024-10-05 07:32:04 +05:00
if rsn_flags & getattr(NM, "80211ApSecurityFlags").KEY_MGMT_PSK:
2024-04-23 07:34:15 +05:00
sec_str += " WPA2"
2024-10-05 07:32:04 +05:00
if rsn_flags & getattr(NM, "80211ApSecurityFlags").KEY_MGMT_SAE:
2024-04-23 07:34:15 +05:00
sec_str += " WPA3"
2024-10-05 07:32:04 +05:00
if (wpa_flags & getattr(NM, "80211ApSecurityFlags").KEY_MGMT_802_1X) or (
rsn_flags & getattr(NM, "80211ApSecurityFlags").KEY_MGMT_802_1X
):
2024-04-23 07:34:15 +05:00
sec_str += " 802.1X"
2024-10-05 07:32:04 +05:00
if (wpa_flags & getattr(NM, "80211ApSecurityFlags").KEY_MGMT_OWE) or (
rsn_flags & getattr(NM, "80211ApSecurityFlags").KEY_MGMT_OWE
):
2024-04-23 07:34:15 +05:00
sec_str += " OWE"
# If there is no security use "--"
if sec_str == "":
sec_str = "--"
return sec_str.lstrip()
2024-10-05 07:32:04 +05:00
class Action: # pylint: disable=too-few-public-methods
2024-04-23 07:34:15 +05:00
"""Helper class to execute functions from a string variable"""
2024-10-05 07:32:04 +05:00
def __init__(self, name, func, args=None, active=False):
2024-04-23 07:34:15 +05:00
self.name = name
self.func = func
self.is_active = active
if args is None:
self.args = None
elif isinstance(args, list):
self.args = args
else:
self.args = [args]
def __str__(self):
return self.name
def __call__(self):
if self.args is None:
self.func()
else:
self.func(*self.args)
def conn_matches_adapter(conn, adapter):
"""Return True if the connection is applicable for the given adapter.
There seem to be two ways for a connection specify what interface it belongs
to:
- By setting 'mac-address' in [wifi] to the adapter's MAC
- By setting 'interface-name` in [connection] to the adapter's name.
Depending on how the connection was added, it seems like either
'mac-address', 'interface-name' or neither of both is set.
"""
# [wifi] mac-address
setting_wireless = conn.get_setting_wireless()
mac = setting_wireless.get_mac_address()
if mac is not None:
return mac == adapter.get_permanent_hw_address()
# [connection] interface-name
setting_connection = conn.get_setting_connection()
interface = setting_connection.get_interface_name()
if interface is not None:
return interface == adapter.get_iface()
# Neither is set, let's assume this connection is for multiple/all adapters.
return True
def process_ap(nm_ap, is_active, adapter):
"""Activate/Deactivate a connection and get password if required"""
if is_active:
CLIENT.deactivate_connection_async(nm_ap, None, deactivate_cb, nm_ap)
LOOP.run()
else:
2024-10-05 07:32:04 +05:00
conns_cur = [
i
for i in CONNS
if i.get_setting_wireless() is not None and conn_matches_adapter(i, adapter)
]
2024-04-23 07:34:15 +05:00
con = nm_ap.filter_connections(conns_cur)
if len(con) > 1:
raise ValueError("There are multiple connections possible")
if len(con) == 1:
2024-10-05 07:32:04 +05:00
CLIENT.activate_connection_async(
con[0], adapter, nm_ap.get_path(), None, activate_cb, nm_ap
)
2024-04-23 07:34:15 +05:00
LOOP.run()
else:
if ap_security(nm_ap) != "--":
password = get_passphrase()
else:
password = ""
set_new_connection(nm_ap, password, adapter)
def activate_cb(dev, res, data):
2024-10-05 07:32:04 +05:00
"""Notification if activate connection completed successfully"""
2024-04-23 07:34:15 +05:00
try:
conn = dev.activate_connection_finish(res)
except GLib.Error:
conn = None
if conn is not None:
notify(f"Activated {conn.get_id()}")
else:
notify(f"Problem activating {data.get_id()}", urgency="critical")
LOOP.quit()
def deactivate_cb(dev, res, data):
2024-10-05 07:32:04 +05:00
"""Notification if deactivate connection completed successfully"""
2024-04-23 07:34:15 +05:00
if dev.deactivate_connection_finish(res) is True:
notify(f"Deactivated {data.get_id()}")
else:
notify(f"Problem deactivating {data.get_id()}", urgency="critical")
LOOP.quit()
def process_vpngsm(con, activate):
"""Activate/deactive VPN or GSM connections"""
if activate:
2024-10-05 07:32:04 +05:00
CLIENT.activate_connection_async(con, None, None, None, activate_cb, con)
2024-04-23 07:34:15 +05:00
else:
CLIENT.deactivate_connection_async(con, None, deactivate_cb, con)
LOOP.run()
2024-10-05 07:32:04 +05:00
2024-04-23 07:34:15 +05:00
def strength_bars(signal_strength):
bars = NM.utils_wifi_strength_bars(signal_strength)
wifi_chars = CONF.get("dmenu", "wifi_chars", fallback=False)
if wifi_chars:
2024-10-05 07:32:04 +05:00
bars = "".join([wifi_chars[i] for i, j in enumerate(bars) if j == "*"])
2024-04-23 07:34:15 +05:00
return bars
def strength_icon(signal_strength):
wifi_icons = CONF.get("dmenu", "wifi_icons", fallback=False)
if wifi_icons:
return wifi_icons[round(signal_strength / 100 * (len(wifi_icons) - 1))]
return ""
def create_ap_actions(aps, active_ap, active_connection, adapter): # noqa pylint: disable=too-many-locals,line-too-long
"""For each AP in a list, create the string and its attached function
(activate/deactivate)
"""
active_ap_bssid = active_ap.get_bssid() if active_ap is not None else ""
names = [ssid_to_utf8(ap) for ap in aps]
max_len_name = max([len(name) for name in names]) if names else 0
secs = [ap_security(ap) for ap in aps]
max_len_sec = max([len(sec) for sec in secs]) if secs else 0
ap_actions = []
if CONF.getboolean("dmenu", "compact", fallback=False):
format = CONF.get("dmenu", "format", fallback="{name} {sec} {bars}")
else:
2024-10-05 07:32:04 +05:00
format = CONF.get(
"dmenu",
"format",
fallback="{name:<{max_len_name}s} {sec:<{max_len_sec}s} {bars:>4}",
)
2024-04-23 07:34:15 +05:00
for nm_ap, name, sec in zip(aps, names, secs):
is_active = nm_ap.get_bssid() == active_ap_bssid
signal_strength = nm_ap.get_strength()
bars = strength_bars(signal_strength)
icon = strength_icon(signal_strength)
2024-10-05 07:32:04 +05:00
action_name = format.format(
name=name,
sec=sec,
signal=signal_strength,
bars=bars,
icon=icon,
max_len_name=max_len_name,
max_len_sec=max_len_sec,
)
2024-04-23 07:34:15 +05:00
if is_active:
2024-10-05 07:32:04 +05:00
ap_actions.append(
Action(
action_name,
process_ap,
[active_connection, True, adapter],
active=True,
)
)
2024-04-23 07:34:15 +05:00
else:
2024-10-05 07:32:04 +05:00
ap_actions.append(Action(action_name, process_ap, [nm_ap, False, adapter]))
2024-04-23 07:34:15 +05:00
return ap_actions
def create_vpn_actions(vpns, active):
"""Create the list of strings to display with associated function
(activate/deactivate) for VPN connections.
"""
active_vpns = [i for i in active if i.get_vpn()]
return _create_vpngsm_actions(vpns, active_vpns, "VPN")
2024-10-05 07:32:04 +05:00
def create_vlan_actions(vlans, active):
"""Create the list of strings to display with associated function
(activate/deactivate) for VLAN connections.
"""
active_vlans = [i for i in active if "vlan" == i.get_connection_type()]
return _create_vpngsm_actions(vlans, active_vlans, "VLAN")
2024-04-23 07:34:15 +05:00
def create_wireguard_actions(wgs, active):
"""Create the list of strings to display with associated function
(activate/deactivate) for Wireguard connections.
"""
active_wgs = [i for i in active if i.get_connection_type() == "wireguard"]
return _create_vpngsm_actions(wgs, active_wgs, "Wireguard")
def create_eth_actions(eths, active):
"""Create the list of strings to display with associated function
(activate/deactivate) for Ethernet connections.
"""
2024-10-05 07:32:04 +05:00
active_eths = [i for i in active if "ethernet" in i.get_connection_type()]
2024-04-23 07:34:15 +05:00
return _create_vpngsm_actions(eths, active_eths, "Eth")
def create_gsm_actions(gsms, active):
"""Create the list of strings to display with associated function
(activate/deactivate) GSM connections."""
2024-10-05 07:32:04 +05:00
active_gsms = [
i
for i in active
if i.get_connection() is not None
and i.get_connection().is_type(NM.SETTING_GSM_SETTING_NAME)
]
2024-04-23 07:34:15 +05:00
return _create_vpngsm_actions(gsms, active_gsms, "GSM")
def create_blue_actions(blues, active):
"""Create the list of strings to display with associated function
(activate/deactivate) Bluetooth connections."""
2024-10-05 07:32:04 +05:00
active_blues = [
i
for i in active
if i.get_connection() is not None
and i.get_connection().is_type(NM.SETTING_BLUETOOTH_SETTING_NAME)
]
2024-04-23 07:34:15 +05:00
return _create_vpngsm_actions(blues, active_blues, "Bluetooth")
def create_saved_actions(saved):
"""Create the list of strings to display with associated function
(activate/deactivate) for VPN connections.
"""
return _create_vpngsm_actions(saved, [], "SAVED")
def _create_vpngsm_actions(cons, active_cons, label):
active_con_ids = [a.get_id() for a in active_cons]
actions = []
for con in cons:
is_active = con.get_id() in active_con_ids
action_name = f"{con.get_id()}:{label}"
if is_active:
2024-10-05 07:32:04 +05:00
active_connection = [a for a in active_cons if a.get_id() == con.get_id()]
2024-04-23 07:34:15 +05:00
if len(active_connection) != 1:
raise ValueError(f"Multiple active connections match {con.get_id()}")
active_connection = active_connection[0]
2024-10-05 07:32:04 +05:00
actions.append(
Action(
action_name, process_vpngsm, [active_connection, False], active=True
)
)
2024-04-23 07:34:15 +05:00
else:
2024-10-05 07:32:04 +05:00
actions.append(Action(action_name, process_vpngsm, [con, True]))
2024-04-23 07:34:15 +05:00
return actions
def create_wwan_actions(client):
2024-10-05 07:32:04 +05:00
"""Create WWWAN actions"""
2024-04-23 07:34:15 +05:00
wwan_enabled = client.wwan_get_enabled()
wwan_action = "Disable" if wwan_enabled else "Enable"
return [Action(f"{wwan_action} WWAN", toggle_wwan, not wwan_enabled)]
2024-10-05 07:32:04 +05:00
def combine_actions(eths, aps, vlans, vpns, wgs, gsms, blues, wwan, others, saved):
2024-04-23 07:34:15 +05:00
# pylint: disable=too-many-arguments
"""Combine all given actions into a list of actions.
Args: args - eths: list of Actions
aps: list of Actions
vpns: list of Actions
gsms: list of Actions
blues: list of Actions
wwan: list of Actions
others: list of Actions
"""
compact = CONF.getboolean("dmenu", "compact", fallback=False)
2024-10-05 07:32:04 +05:00
empty_action = [Action("", None)] if not compact else []
2024-04-23 07:34:15 +05:00
all_actions = []
all_actions += eths + empty_action if eths else []
all_actions += aps + empty_action if aps else []
2024-10-05 07:32:04 +05:00
all_actions += vlans + empty_action if vlans else []
2024-04-23 07:34:15 +05:00
all_actions += vpns + empty_action if vpns else []
all_actions += wgs + empty_action if wgs else []
all_actions += gsms + empty_action if (gsms and wwan) else []
all_actions += blues + empty_action if blues else []
all_actions += wwan + empty_action if wwan else []
all_actions += others + empty_action if others else []
all_actions += saved + empty_action if saved else []
return all_actions
2024-10-05 07:32:04 +05:00
def get_wofi_highlight_markup(action):
highlight_fg = CONF.get("dmenu", "highlight_fg", fallback=None)
highlight_bg = CONF.get("dmenu", "highlight_bg", fallback=None)
highlight_bold = CONF.getboolean("dmenu", "highlight_bold", fallback=True)
style = ""
if highlight_fg:
style += f'foreground="{highlight_fg}" '
if highlight_bg:
style += f'background="{highlight_bg}" '
if highlight_bold:
style += 'weight="bold" '
return f"<span {style}>" + str(action) + "</span>"
2024-04-23 07:34:15 +05:00
def get_selection(all_actions):
"""Spawn dmenu for selection and execute the associated action."""
2024-10-05 07:32:04 +05:00
command = shlex.split(CONF.get("dmenu", "dmenu_command", fallback="dmenu"))
cmd_base = basename(command[0])
active_chars = CONF.get("dmenu", "active_chars", fallback="==")
highlight = CONF.getboolean("dmenu", "highlight", fallback=False)
2024-04-23 07:34:15 +05:00
inp = []
2024-10-05 07:32:04 +05:00
if highlight is True and cmd_base == "rofi":
2024-04-23 07:34:15 +05:00
inp = [str(action) for action in all_actions]
2024-10-05 07:32:04 +05:00
elif highlight is True and cmd_base == "wofi":
inp = [
get_wofi_highlight_markup(action) if action.is_active else str(action)
for action in all_actions
]
2024-04-23 07:34:15 +05:00
else:
2024-10-05 07:32:04 +05:00
inp = [
(active_chars if action.is_active else " " * len(active_chars))
+ " "
+ str(action)
for action in all_actions
]
active_lines = [
index for index, action in enumerate(all_actions) if action.is_active
]
2024-04-23 07:34:15 +05:00
command = dmenu_cmd(len(inp), active_lines=active_lines)
2024-10-05 07:32:04 +05:00
sel = subprocess.run(
command,
capture_output=True,
check=False,
input="\n".join(inp),
encoding=ENC,
env=ENV,
).stdout
2024-04-23 07:34:15 +05:00
if not sel.rstrip():
sys.exit()
2024-10-05 07:32:04 +05:00
if highlight is True and cmd_base == "rofi":
2024-04-23 07:34:15 +05:00
action = [i for i in all_actions if str(i).strip() == sel.strip()]
2024-10-05 07:32:04 +05:00
elif highlight is True and cmd_base == "wofi":
action = [
i
for i in all_actions
if str(i).strip() == sel.strip()
or get_wofi_highlight_markup(i) == sel.strip()
]
else:
action = [
i
for i in all_actions
if (
(str(i).strip() == str(sel.strip()) and not i.is_active)
or (
active_chars + " " + str(i) == str(sel.rstrip("\n")) and i.is_active
)
)
]
2024-04-23 07:34:15 +05:00
if len(action) != 1:
raise ValueError(f"Selection was ambiguous: '{str(sel.strip())}'")
return action[0]
def toggle_networking(enable):
"""Enable/disable networking
Args: enable - boolean
"""
toggle = GLib.Variant.new_tuple(GLib.Variant.new_boolean(enable))
try:
2024-10-05 07:32:04 +05:00
CLIENT.dbus_call(
NM.DBUS_PATH,
NM.DBUS_INTERFACE,
"Enable",
toggle,
None,
-1,
None,
None,
None,
)
2024-04-23 07:34:15 +05:00
except AttributeError:
# Workaround for older versions of python-gobject
CLIENT.networking_set_enabled(enable)
notify(f"Networking {'enabled' if enable is True else 'disabled'}")
def toggle_wifi(enable):
"""Enable/disable Wifi
Args: enable - boolean
"""
toggle = GLib.Variant.new_boolean(enable)
try:
2024-10-05 07:32:04 +05:00
CLIENT.dbus_set_property(
NM.DBUS_PATH,
NM.DBUS_INTERFACE,
"WirelessEnabled",
toggle,
-1,
None,
None,
None,
)
2024-04-23 07:34:15 +05:00
except AttributeError:
# Workaround for older versions of python-gobject
CLIENT.wireless_set_enabled(enable)
notify(f"Wifi {'enabled' if enable is True else 'disabled'}")
def toggle_wwan(enable):
"""Enable/disable WWAN
Args: enable - boolean
"""
toggle = GLib.Variant.new_boolean(enable)
try:
2024-10-05 07:32:04 +05:00
CLIENT.dbus_set_property(
NM.DBUS_PATH, NM.DBUS_INTERFACE, "WwanEnabled", toggle, -1, None, None, None
)
2024-04-23 07:34:15 +05:00
except AttributeError:
# Workaround for older versions of python-gobject
CLIENT.wwan_set_enabled(enable)
notify(f"Wwan {'enabled' if enable is True else 'disabled'}")
def toggle_bluetooth(enable):
"""Enable/disable Bluetooth
2024-10-05 07:32:04 +05:00
Try bluetoothctl first, then drop to rfkill if it's not installed or
bluetooth service isn't running.
2024-04-23 07:34:15 +05:00
Args: enable - boolean
References:
https://github.com/blueman-project/blueman/blob/master/blueman/plugins/mechanism/RfKill.py
https://www.kernel.org/doc/html/latest/driver-api/rfkill.html
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/uapi/linux/rfkill.h?h=v5.8.9
"""
2024-10-05 07:32:04 +05:00
if is_installed("bluetoothctl") and is_running("bluetoothd"):
# Times out in 2 seconds, otherwise bluetoothctl will hang if bluetooth
# service isn't running.
try:
res = subprocess.run(
["bluetoothctl", "power", "on" if enable is True else "off"],
timeout=2,
capture_output=True,
)
except subprocess.TimeoutExpired:
pass
try:
res = subprocess.run(
["bluetoothctl", "show"], timeout=2, capture_output=True, text=True
)
if "Powered: yes" in res.stdout:
notify("Bluetooth enabled")
return
except subprocess.TimeoutExpired:
pass
# Now try using rfkill
2024-04-23 07:34:15 +05:00
type_bluetooth = 2
op_change_all = 3
idx = 0
soft_state = 0 if enable else 1
hard_state = 0
2024-10-05 07:32:04 +05:00
data = struct.pack(
"IBBBB", idx, type_bluetooth, op_change_all, soft_state, hard_state
)
2024-04-23 07:34:15 +05:00
try:
2024-10-05 07:32:04 +05:00
with open("/dev/rfkill", "r+b", buffering=0) as rff:
2024-04-23 07:34:15 +05:00
rff.write(data)
except PermissionError:
2024-10-05 07:32:04 +05:00
notify(
"Lacking permission to write to /dev/rfkill.",
"Check README for configuration options.",
urgency="critical",
)
2024-04-23 07:34:15 +05:00
else:
notify(f"Bluetooth {'enabled' if enable else 'disabled'}")
def launch_connection_editor():
2024-10-05 07:32:04 +05:00
"""Launch nmtui or the gui nm-connection-editor"""
terminal = shlex.split(CONF.get("editor", "terminal", fallback="xterm"))
2024-04-23 07:34:15 +05:00
gui_if_available = CONF.getboolean("editor", "gui_if_available", fallback=True)
2024-10-05 07:32:04 +05:00
gui = CONF.get("editor", "gui", fallback="nm-connection-editor")
2024-04-23 07:34:15 +05:00
if gui_if_available is True:
2024-10-05 07:32:04 +05:00
if is_installed(gui):
subprocess.run(gui, check=False)
return
2024-04-23 07:34:15 +05:00
if is_installed("nmtui"):
2024-10-05 07:32:04 +05:00
subprocess.run(terminal + ["-e", "nmtui"], check=False)
2024-04-23 07:34:15 +05:00
return
notify("No network connection editor installed", urgency="critical")
def get_passphrase():
"""Get a password
Returns: string
"""
pinentry = CONF.get("dmenu", "pinentry", fallback=None)
if pinentry:
2024-10-05 07:32:04 +05:00
description = CONF.get(
"pinentry", "description", fallback="Get network password"
)
2024-04-23 07:34:15 +05:00
prompt = CONF.get("pinentry", "prompt", fallback="Password: ")
pin = ""
2024-10-05 07:32:04 +05:00
out = subprocess.run(
pinentry,
capture_output=True,
check=False,
encoding=ENC,
input=f"setdesc {description}\nsetprompt {prompt}\ngetpin\n",
).stdout
2024-04-23 07:34:15 +05:00
if out:
res = [i for i in out.split("\n") if i.startswith("D ")]
if res and res[0].startswith("D "):
pin = res[0].split("D ")[1]
return pin
2024-10-05 07:32:04 +05:00
return subprocess.run(
dmenu_cmd(0, "Passphrase"),
stdin=subprocess.DEVNULL,
capture_output=True,
check=False,
encoding=ENC,
).stdout
2024-04-23 07:34:15 +05:00
def delete_connection():
2024-10-05 07:32:04 +05:00
"""Display list of NM connections and delete the selected one"""
conn_acts = [
Action(i.get_id(), i.delete_async, args=[None, delete_cb, None]) for i in CONNS
]
2024-04-23 07:34:15 +05:00
conn_names = "\n".join([str(i) for i in conn_acts])
2024-10-05 07:32:04 +05:00
sel = subprocess.run(
dmenu_cmd(len(conn_acts), "CHOOSE CONNECTION TO DELETE:"),
capture_output=True,
check=False,
input=conn_names,
encoding=ENC,
env=ENV,
).stdout
2024-04-23 07:34:15 +05:00
if not sel.strip():
sys.exit()
action = [i for i in conn_acts if str(i) == sel.rstrip("\n")]
if len(action) != 1:
raise ValueError(f"Selection was ambiguous: {str(sel)}")
action[0]()
LOOP.run()
def delete_cb(dev, res, data):
2024-10-05 07:32:04 +05:00
"""Notification if delete completed successfully"""
2024-04-23 07:34:15 +05:00
if dev.delete_finish(res) is True:
notify(f"Deleted {dev.get_id()}")
else:
notify(f"Problem deleting {dev.get_id()}", urgency="critical")
LOOP.quit()
def set_new_connection(nm_ap, nm_pw, adapter):
"""Setup a new NetworkManager connection
Args: ap - NM.AccessPoint
pw - string
"""
nm_pw = str(nm_pw).strip()
profile = create_wifi_profile(nm_ap, nm_pw, adapter)
2024-10-05 07:32:04 +05:00
CLIENT.add_and_activate_connection_async(
profile, adapter, nm_ap.get_path(), None, verify_conn, profile
)
2024-04-23 07:34:15 +05:00
LOOP.run()
def create_wifi_profile(nm_ap, password, adapter):
# pylint: disable=line-too-long
# noqa From https://cgit.freedesktop.org/NetworkManager/NetworkManager/tree/examples/python/gi/add_connection.py
# noqa and https://cgit.freedesktop.org/NetworkManager/NetworkManager/tree/examples/python/dbus/add-wifi-psk-connection.py
# pylint: enable=line-too-long
"""Create the NM profile given the AP and passphrase"""
ap_sec = ap_security(nm_ap)
profile = NM.SimpleConnection.new()
s_con = NM.SettingConnection.new()
s_con.set_property(NM.SETTING_CONNECTION_ID, ssid_to_utf8(nm_ap))
s_con.set_property(NM.SETTING_CONNECTION_UUID, str(uuid.uuid4()))
s_con.set_property(NM.SETTING_CONNECTION_TYPE, "802-11-wireless")
profile.add_setting(s_con)
s_wifi = NM.SettingWireless.new()
s_wifi.set_property(NM.SETTING_WIRELESS_SSID, nm_ap.get_ssid())
2024-10-05 07:32:04 +05:00
s_wifi.set_property(NM.SETTING_WIRELESS_MODE, "infrastructure")
s_wifi.set_property(
NM.SETTING_WIRELESS_MAC_ADDRESS, adapter.get_permanent_hw_address()
)
2024-04-23 07:34:15 +05:00
profile.add_setting(s_wifi)
s_ip4 = NM.SettingIP4Config.new()
s_ip4.set_property(NM.SETTING_IP_CONFIG_METHOD, "auto")
profile.add_setting(s_ip4)
s_ip6 = NM.SettingIP6Config.new()
s_ip6.set_property(NM.SETTING_IP_CONFIG_METHOD, "auto")
profile.add_setting(s_ip6)
if ap_sec != "--":
s_wifi_sec = NM.SettingWirelessSecurity.new()
if "WPA" in ap_sec:
if "WPA3" in ap_sec:
2024-10-05 07:32:04 +05:00
s_wifi_sec.set_property(NM.SETTING_WIRELESS_SECURITY_KEY_MGMT, "sae")
2024-04-23 07:34:15 +05:00
else:
2024-10-05 07:32:04 +05:00
s_wifi_sec.set_property(
NM.SETTING_WIRELESS_SECURITY_KEY_MGMT, "wpa-psk"
)
s_wifi_sec.set_property(NM.SETTING_WIRELESS_SECURITY_AUTH_ALG, "open")
2024-04-23 07:34:15 +05:00
s_wifi_sec.set_property(NM.SETTING_WIRELESS_SECURITY_PSK, password)
elif "WEP" in ap_sec:
2024-10-05 07:32:04 +05:00
s_wifi_sec.set_property(NM.SETTING_WIRELESS_SECURITY_KEY_MGMT, "None")
s_wifi_sec.set_property(
NM.SETTING_WIRELESS_SECURITY_WEP_KEY_TYPE, NM.WepKeyType.PASSPHRASE
)
2024-04-23 07:34:15 +05:00
s_wifi_sec.set_wep_key(0, password)
profile.add_setting(s_wifi_sec)
return profile
def verify_conn(client, result, data):
"""Callback function for add_and_activate_connection_async
Check if connection completes successfully. Delete the connection if there
is an error.
"""
try:
act_conn = client.add_and_activate_connection_finish(result)
conn = act_conn.get_connection()
2024-10-05 07:32:04 +05:00
if not all(
[conn.verify(), conn.verify_secrets(), data.verify(), data.verify_secrets()]
):
2024-04-23 07:34:15 +05:00
raise GLib.Error
notify(f"Added {conn.get_id()}")
except GLib.Error:
try:
2024-10-05 07:32:04 +05:00
notify(f"Connection to {conn.get_id()} failed", urgency="critical")
2024-04-23 07:34:15 +05:00
conn.delete_async(None, None, None)
except UnboundLocalError:
pass
finally:
LOOP.quit()
def create_ap_list(adapter, active_connections):
"""Generate list of access points. Remove duplicate APs , keeping strongest
ones and the active AP
Args: adapter
active_connections - list of all active connections
Returns: aps - list of access points
active_ap - active AP
active_ap_con - active Connection
adapter
"""
aps = []
ap_names = []
active_ap = adapter.get_active_access_point()
2024-10-05 07:32:04 +05:00
aps_all = sorted(
adapter.get_access_points(), key=lambda a: a.get_strength(), reverse=True
)
conns_cur = [
i
for i in CONNS
if i.get_setting_wireless() is not None and conn_matches_adapter(i, adapter)
]
2024-04-23 07:34:15 +05:00
try:
ap_conns = active_ap.filter_connections(conns_cur)
active_ap_name = ssid_to_utf8(active_ap)
2024-10-05 07:32:04 +05:00
active_ap_con = [
active_conn
for active_conn in active_connections
if active_conn.get_connection() in ap_conns
]
2024-04-23 07:34:15 +05:00
except AttributeError:
active_ap_name = None
active_ap_con = []
if len(active_ap_con) > 1:
2024-10-05 07:32:04 +05:00
raise ValueError("Multiple connection profiles match" " the wireless AP")
2024-04-23 07:34:15 +05:00
active_ap_con = active_ap_con[0] if active_ap_con else None
for nm_ap in aps_all:
ap_name = ssid_to_utf8(nm_ap)
if nm_ap != active_ap and ap_name == active_ap_name:
# Skip adding AP if it's not active but same name as active AP
continue
if ap_name not in ap_names:
ap_names.append(ap_name)
aps.append(nm_ap)
return aps, active_ap, active_ap_con, adapter
def notify(message, details=None, urgency="low"):
2024-10-05 07:32:04 +05:00
"""Use notify-send if available for notifications"""
delay = CONF.getint("nmdm", "rescan_delay", fallback=5)
args = [
"-u",
urgency,
"-a",
"networkmanager-dmenu",
"-t",
str(delay * 1000),
message,
]
2024-04-23 07:34:15 +05:00
if details is not None:
args.append(details)
if is_installed("notify-send"):
subprocess.run(["notify-send"] + args, check=False)
def run(): # pylint: disable=too-many-locals
"""Main script entrypoint"""
try:
subprocess.check_output(["pidof", "NetworkManager"])
except subprocess.CalledProcessError:
notify("WARNING: NetworkManager don't seems to be running")
print("WARNING: NetworkManager don't seems to be running")
active = CLIENT.get_active_connections()
adapter = choose_adapter(CLIENT)
if adapter:
ap_actions = create_ap_actions(*create_ap_list(adapter, active))
else:
ap_actions = []
vpns = [i for i in CONNS if i.is_type(NM.SETTING_VPN_SETTING_NAME)]
try:
wgs = [i for i in CONNS if i.is_type(NM.SETTING_WIREGUARD_SETTING_NAME)]
except AttributeError:
# Workaround for older versions of python-gobject with no wireguard support
wgs = []
eths = [i for i in CONNS if i.is_type(NM.SETTING_WIRED_SETTING_NAME)]
2024-10-05 07:32:04 +05:00
vlans = [i for i in CONNS if i.is_type(NM.SETTING_VLAN_SETTING_NAME)]
2024-04-23 07:34:15 +05:00
blues = [i for i in CONNS if i.is_type(NM.SETTING_BLUETOOTH_SETTING_NAME)]
vpn_actions = create_vpn_actions(vpns, active)
wg_actions = create_wireguard_actions(wgs, active)
eth_actions = create_eth_actions(eths, active)
2024-10-05 07:32:04 +05:00
vlan_actions = create_vlan_actions(vlans, active)
2024-04-23 07:34:15 +05:00
blue_actions = create_blue_actions(blues, active)
other_actions = create_other_actions(CLIENT)
wwan_installed = is_installed("ModemManager")
if wwan_installed:
gsms = [i for i in CONNS if i.is_type(NM.SETTING_GSM_SETTING_NAME)]
gsm_actions = create_gsm_actions(gsms, active)
wwan_actions = create_wwan_actions(CLIENT)
else:
gsm_actions = []
wwan_actions = []
2024-10-05 07:32:04 +05:00
list_saved = CONF.getboolean("dmenu", "list_saved", fallback=False)
2024-04-23 07:34:15 +05:00
saved_cons = [i for i in CONNS if i not in vpns + wgs + eths + blues]
if list_saved:
saved_actions = create_saved_actions(saved_cons)
else:
saved_actions = [Action("Saved connections", prompt_saved, [saved_cons])]
2024-10-05 07:32:04 +05:00
actions = combine_actions(
eth_actions,
ap_actions,
vlan_actions,
vpn_actions,
wg_actions,
gsm_actions,
blue_actions,
wwan_actions,
other_actions,
saved_actions,
)
2024-04-23 07:34:15 +05:00
sel = get_selection(actions)
sel()
def main():
2024-10-05 07:32:04 +05:00
"""Main. Enables script to be re-run after a wifi rescan"""
2024-04-23 07:34:15 +05:00
global CLIENT, CONNS, LOOP # noqa pylint: disable=global-variable-undefined
CLIENT = NM.Client.new(None)
LOOP = GLib.MainLoop()
CONNS = CLIENT.get_connections()
run()
2024-10-05 07:32:04 +05:00
if __name__ == "__main__":
2024-04-23 07:34:15 +05:00
main()
# vim: set et ts=4 sw=4 :