From cc910f1198fa8a9b3702d72046c9fe4d7637a56a Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Fri, 9 Sep 2022 21:21:09 +0200 Subject: [PATCH 1/9] [modules/pulsectl] add preliminary version of event-based pulseaudio add a new module based on pulsectl, with pulsein for microphone and pulseout for speakers. should eventually become a drop-in replacement for pasink and pasource. see #917 --- bumblebee_status/modules/core/pulsectl.py | 44 +++++++++++++++++++++++ bumblebee_status/modules/core/pulsein.py | 9 +++++ bumblebee_status/modules/core/pulseout.py | 9 +++++ themes/icons/ascii.json | 16 +++++++++ themes/icons/awesome-fonts.json | 18 +++++++++- 5 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 bumblebee_status/modules/core/pulsectl.py create mode 100644 bumblebee_status/modules/core/pulsein.py create mode 100644 bumblebee_status/modules/core/pulseout.py diff --git a/bumblebee_status/modules/core/pulsectl.py b/bumblebee_status/modules/core/pulsectl.py new file mode 100644 index 0000000..70fc356 --- /dev/null +++ b/bumblebee_status/modules/core/pulsectl.py @@ -0,0 +1,44 @@ +# pylint: disable=C0111,R0903 + +import pulsectl + +import core.module +import core.widget +import core.decorators +import core.input +import core.event + +class Module(core.module.Module): + def __init__(self, config, theme, type): + super().__init__(config, theme, core.widget.Widget(self.display)) + self.background = True + + self.__type = type + self.__volume = "n/a" + self.__muted = False + + self.process(None) + + def display(self, _): + return f"{int(self.__volume*100)}%" + + def process(self, _): + with pulsectl.Pulse(self.id + "proc") as pulse: + dev = pulse.sink_list()[0] if self.__type == "sink" else pulse.source_list()[0] + self.__volume = dev.volume.value_flat + self.__muted = dev.mute + core.event.trigger("update", [self.id], redraw_only=True) + core.event.trigger("draw") + + def update(self): + with pulsectl.Pulse(self.id) as pulse: + pulse.event_mask_set(self.__type) + pulse.event_callback_set(self.process) + pulse.event_listen() + + def state(self, _): + if self.__muted: + return ["warning", "muted"] + return ["unmuted"] + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/core/pulsein.py b/bumblebee_status/modules/core/pulsein.py new file mode 100644 index 0000000..0034dd0 --- /dev/null +++ b/bumblebee_status/modules/core/pulsein.py @@ -0,0 +1,9 @@ +from .pulsectl import Module + + +class Module(Module): + def __init__(self, config, theme): + super().__init__(config, theme, "source") + + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/core/pulseout.py b/bumblebee_status/modules/core/pulseout.py new file mode 100644 index 0000000..31fecec --- /dev/null +++ b/bumblebee_status/modules/core/pulseout.py @@ -0,0 +1,9 @@ +from .pulsectl import Module + + +class Module(Module): + def __init__(self, config, theme): + super().__init__(config, theme, "sink") + + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/themes/icons/ascii.json b/themes/icons/ascii.json index 77ef931..089fa3c 100644 --- a/themes/icons/ascii.json +++ b/themes/icons/ascii.json @@ -64,6 +64,14 @@ "prefix": "audio" } }, + "pulseout": { + "muted": { + "prefix": "audio(mute)" + }, + "unmuted": { + "prefix": "audio" + } + }, "amixer": { "muted": { "prefix": "audio(mute)" @@ -80,6 +88,14 @@ "prefix": "mic" } }, + "pulsein": { + "muted": { + "prefix": "mic(mute)" + }, + "unmuted": { + "prefix": "mic" + } + }, "nic": { "wireless-up": { "prefix": "wifi" diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json index a3de1d2..acd1fc3 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -197,6 +197,14 @@ "prefix": "" } }, + "pulseout": { + "muted": { + "prefix": "" + }, + "unmuted": { + "prefix": "" + } + }, "amixer": { "muted": { "prefix": "" @@ -213,6 +221,14 @@ "prefix": "" } }, + "pulsein": { + "muted": { + "prefix": "" + }, + "unmuted": { + "prefix": "" + } + }, "kernel": { "prefix": "\uf17c" }, @@ -707,4 +723,4 @@ "thunderbird": { "prefix": "" } -} \ No newline at end of file +} From a1d94d4355a7553dfd9de793f4d50d7be5f072e7 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Fri, 9 Sep 2022 21:34:32 +0200 Subject: [PATCH 2/9] [modules/pulsectl] add mouse actions add toggle mute on click and volume up/down on scroll --- bumblebee_status/modules/core/pulsectl.py | 48 ++++++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/modules/core/pulsectl.py b/bumblebee_status/modules/core/pulsectl.py index 70fc356..46f3775 100644 --- a/bumblebee_status/modules/core/pulsectl.py +++ b/bumblebee_status/modules/core/pulsectl.py @@ -4,10 +4,11 @@ import pulsectl import core.module import core.widget -import core.decorators import core.input import core.event +import util.format + class Module(core.module.Module): def __init__(self, config, theme, type): super().__init__(config, theme, core.widget.Widget(self.display)) @@ -17,14 +18,57 @@ class Module(core.module.Module): self.__volume = "n/a" self.__muted = False + self.__change = util.format.asint( + self.parameter("percent_change", "2%").strip("%"), 0, 100 + ) + + events = [ + { + "type": "mute", + "action": self.toggle_mute, + "button": core.input.LEFT_MOUSE + }, + { + "type": "volume", + "action": self.increase_volume, + "button": core.input.WHEEL_UP, + }, + { + "type": "volume", + "action": self.decrease_volume, + "button": core.input.WHEEL_DOWN, + }, + ] + + for event in events: + core.input.register(self, button=event["button"], cmd=event["action"]) + self.process(None) def display(self, _): return f"{int(self.__volume*100)}%" + def toggle_mute(self, _): + with pulsectl.Pulse(self.id + "vol") as pulse: + dev = pulse.sink_list()[0] if self.__type == "sink" else pulse.source_list()[1] + pulse.mute(dev, not self.__muted) + + def change_volume(self, amount): + with pulsectl.Pulse(self.id + "vol") as pulse: + dev = pulse.sink_list()[0] if self.__type == "sink" else pulse.source_list()[1] + vol = dev.volume + vol.value_flat += amount + pulse.volume_set(dev, vol) + + def increase_volume(self, _): + self.change_volume(self.__change/100.0) + + def decrease_volume(self, _): + self.change_volume(-self.__change/100.0) + def process(self, _): with pulsectl.Pulse(self.id + "proc") as pulse: - dev = pulse.sink_list()[0] if self.__type == "sink" else pulse.source_list()[0] + dev = pulse.sink_list()[0] if self.__type == "sink" else pulse.source_list()[1] self.__volume = dev.volume.value_flat self.__muted = dev.mute core.event.trigger("update", [self.id], redraw_only=True) From 003a6efc8e6b929834ffb2a7ca419dbe5bfc8073 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Fri, 9 Sep 2022 21:40:03 +0200 Subject: [PATCH 3/9] [modules/pulsectl] figure out default devices make sure the modules always refer to the default devices --- bumblebee_status/modules/core/pulsectl.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/bumblebee_status/modules/core/pulsectl.py b/bumblebee_status/modules/core/pulsectl.py index 46f3775..364156b 100644 --- a/bumblebee_status/modules/core/pulsectl.py +++ b/bumblebee_status/modules/core/pulsectl.py @@ -50,12 +50,12 @@ class Module(core.module.Module): def toggle_mute(self, _): with pulsectl.Pulse(self.id + "vol") as pulse: - dev = pulse.sink_list()[0] if self.__type == "sink" else pulse.source_list()[1] + dev = self.get_device(pulse) pulse.mute(dev, not self.__muted) def change_volume(self, amount): with pulsectl.Pulse(self.id + "vol") as pulse: - dev = pulse.sink_list()[0] if self.__type == "sink" else pulse.source_list()[1] + dev = self.get_device(pulse) vol = dev.volume vol.value_flat += amount pulse.volume_set(dev, vol) @@ -66,9 +66,20 @@ class Module(core.module.Module): def decrease_volume(self, _): self.change_volume(-self.__change/100.0) + def get_device(self, pulse): + devs = pulse.sink_list() if self.__type == "sink" else pulse.source_list() + default = pulse.server_info().default_sink_name if self.__type == "sink" else pulse.server_info().default_source_name + + for dev in devs: + if dev.name == default: + return dev + return devs[0] # fallback + + + def process(self, _): with pulsectl.Pulse(self.id + "proc") as pulse: - dev = pulse.sink_list()[0] if self.__type == "sink" else pulse.source_list()[1] + dev = self.get_device(pulse) self.__volume = dev.volume.value_flat self.__muted = dev.mute core.event.trigger("update", [self.id], redraw_only=True) From ca6bf2e189958044f0f4de7e757cd660ec83617e Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 10 Sep 2022 08:57:00 +0200 Subject: [PATCH 4/9] [modules/pulsectl] add option to automatically start pulseaudio --- bumblebee_status/modules/core/pulsectl.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bumblebee_status/modules/core/pulsectl.py b/bumblebee_status/modules/core/pulsectl.py index 364156b..8d36646 100644 --- a/bumblebee_status/modules/core/pulsectl.py +++ b/bumblebee_status/modules/core/pulsectl.py @@ -7,6 +7,7 @@ import core.widget import core.input import core.event +import util.cli import util.format class Module(core.module.Module): @@ -43,6 +44,9 @@ class Module(core.module.Module): for event in events: core.input.register(self, button=event["button"], cmd=event["action"]) + if util.format.asbool(self.parameter("autostart", False)): + util.cli.execute("pulseaudio --start", ignore_errors=True) + self.process(None) def display(self, _): From 20bc4b3fa6ef319b766339657095772804da3cb2 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 10 Sep 2022 09:01:21 +0200 Subject: [PATCH 5/9] [modules/pulsectl] add parameter to set an upper limit --- bumblebee_status/modules/core/pulsectl.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bumblebee_status/modules/core/pulsectl.py b/bumblebee_status/modules/core/pulsectl.py index 8d36646..f8fa6a2 100644 --- a/bumblebee_status/modules/core/pulsectl.py +++ b/bumblebee_status/modules/core/pulsectl.py @@ -22,6 +22,7 @@ class Module(core.module.Module): self.__change = util.format.asint( self.parameter("percent_change", "2%").strip("%"), 0, 100 ) + self.__limit = util.format.asint(self.parameter("limit", "0%").strip("%"), 0) events = [ { @@ -62,6 +63,8 @@ class Module(core.module.Module): dev = self.get_device(pulse) vol = dev.volume vol.value_flat += amount + if vol.value_flat > self.__limit/100: + vol.value_flat = self.__limit/100 pulse.volume_set(dev, vol) def increase_volume(self, _): From 025b1ec2f2f953fdd53e8fb7d4380b1e1c6c0c55 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 10 Sep 2022 09:04:52 +0200 Subject: [PATCH 6/9] [modules/pulsectl] add optional bar representation --- bumblebee_status/modules/core/pulsectl.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/modules/core/pulsectl.py b/bumblebee_status/modules/core/pulsectl.py index f8fa6a2..fd2c8be 100644 --- a/bumblebee_status/modules/core/pulsectl.py +++ b/bumblebee_status/modules/core/pulsectl.py @@ -8,6 +8,7 @@ import core.input import core.event import util.cli +import util.graph import util.format class Module(core.module.Module): @@ -18,6 +19,7 @@ class Module(core.module.Module): self.__type = type self.__volume = "n/a" self.__muted = False + self.__showbars = util.format.asbool(self.parameter("showbars", False)) self.__change = util.format.asint( self.parameter("percent_change", "2%").strip("%"), 0, 100 @@ -51,7 +53,10 @@ class Module(core.module.Module): self.process(None) def display(self, _): - return f"{int(self.__volume*100)}%" + res = f"{int(self.__volume*100)}%" + if self.__showbars: + res = f"{res} {util.graph.hbar(self.__volume*100)}" + return res def toggle_mute(self, _): with pulsectl.Pulse(self.id + "vol") as pulse: @@ -63,7 +68,7 @@ class Module(core.module.Module): dev = self.get_device(pulse) vol = dev.volume vol.value_flat += amount - if vol.value_flat > self.__limit/100: + if self.__limit > 0 and vol.value_flat > self.__limit/100: vol.value_flat = self.__limit/100 pulse.volume_set(dev, vol) From eb11c279f6e87d47f27b861ea9b7ecd8a9a5b162 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 10 Sep 2022 09:09:16 +0200 Subject: [PATCH 7/9] [modules/pulsectl] add device name mapping and display --- bumblebee_status/modules/core/pulsectl.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/bumblebee_status/modules/core/pulsectl.py b/bumblebee_status/modules/core/pulsectl.py index fd2c8be..c60252e 100644 --- a/bumblebee_status/modules/core/pulsectl.py +++ b/bumblebee_status/modules/core/pulsectl.py @@ -18,8 +18,12 @@ class Module(core.module.Module): self.__type = type self.__volume = "n/a" + self.__devicename = "n/a" self.__muted = False self.__showbars = util.format.asbool(self.parameter("showbars", False)) + self.__show_device_name = util.format.asbool( + self.parameter("showdevicename", False) + ) self.__change = util.format.asint( self.parameter("percent_change", "2%").strip("%"), 0, 100 @@ -56,6 +60,15 @@ class Module(core.module.Module): res = f"{int(self.__volume*100)}%" if self.__showbars: res = f"{res} {util.graph.hbar(self.__volume*100)}" + + if self.__show_device_name: + friendly_name = self.__devicename + icon = self.parameter("icon." + self.__devicename, "") + res = ( + icon + " " + friendly_name + " | " + res + if icon != "" + else friendly_name + " | " + res + ) return res def toggle_mute(self, _): @@ -94,6 +107,7 @@ class Module(core.module.Module): dev = self.get_device(pulse) self.__volume = dev.volume.value_flat self.__muted = dev.mute + self.__devicename = dev.name core.event.trigger("update", [self.id], redraw_only=True) core.event.trigger("draw") From a97f46c0879c5a545bc03eeae547b6121d72d36b Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 10 Sep 2022 09:12:03 +0200 Subject: [PATCH 8/9] [modules/pulsectl] add documentation --- bumblebee_status/modules/core/pulsectl.py | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/bumblebee_status/modules/core/pulsectl.py b/bumblebee_status/modules/core/pulsectl.py index c60252e..52d8907 100644 --- a/bumblebee_status/modules/core/pulsectl.py +++ b/bumblebee_status/modules/core/pulsectl.py @@ -1,5 +1,37 @@ # pylint: disable=C0111,R0903 +"""Displays volume and mute status and controls for PulseAudio devices. Use wheel up and down to change volume, left click mutes, right click opens pavucontrol. + +Aliases: pulseout (for outputs, such as headsets, speakers), pulsein (for microphones) + +NOTE: Do **not** use this module directly, but rather use either pulseout or pulsein! +NOTE2: For the parameter names below, please also use pulseout or pulsein, instead of pulsectl + +Parameters: + * pulsectl.autostart: If set to 'true' (default is 'false'), automatically starts the pulsectl daemon if it is not running + * pulsectl.percent_change: How much to change volume by when scrolling on the module (default is 2%) + * pulsectl.limit: Upper limit for setting the volume (default is 0%, which means 'no limit') + * pulsectl.showbars: 'true' for showing volume bars, requires --markup=pango; + 'false' for not showing volume bars (default) + * pulsectl.showdevicename: If set to 'true' (default is 'false'), the currently selected default device is shown. + Per default, the sink/source name returned by "pactl list sinks short" is used as display name. + + As this name is usually not particularly nice (e.g "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo"), + its possible to map the name to more a user friendly name. + + e.g to map "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" to the name "Headset", add the following + bumblebee-status config entry: pulsectl.alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo=Headset + + Furthermore its possible to specify individual (unicode) icons for all sinks/sources. e.g in order to use the icon 🎧 for the + "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" sink, add the following bumblebee-status config entry: + pulsectl.icon.alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo=🎧 + * Per default a left mouse button click mutes/unmutes the device. In case you want to open a dropdown menu to change the current + default device add the following config entry to your bumblebee-status config: pulsectl.left-click=select_default_device_popup + +Requires the following Python module: + * pulsectl +""" + import pulsectl import core.module From 2287dcab4864a2f0471d0eceaff057850f39af0c Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 10 Sep 2022 09:13:55 +0200 Subject: [PATCH 9/9] [docs] add note for pulsectl being preferred over pulseaudio --- bumblebee_status/modules/core/pulseaudio.py | 2 ++ bumblebee_status/modules/core/pulsectl.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/bumblebee_status/modules/core/pulseaudio.py b/bumblebee_status/modules/core/pulseaudio.py index 70acb01..431de10 100644 --- a/bumblebee_status/modules/core/pulseaudio.py +++ b/bumblebee_status/modules/core/pulseaudio.py @@ -2,6 +2,8 @@ """Displays volume and mute status and controls for PulseAudio devices. Use wheel up and down to change volume, left click mutes, right click opens pavucontrol. +!!! This module will eventually be deprecated (since it has bad performance and high CPU load) and be replaced with "pulsectl", which is a much better drop-in replacement !!! + Aliases: pasink (use this to control output instead of input), pasource Parameters: diff --git a/bumblebee_status/modules/core/pulsectl.py b/bumblebee_status/modules/core/pulsectl.py index 52d8907..f76a38c 100644 --- a/bumblebee_status/modules/core/pulsectl.py +++ b/bumblebee_status/modules/core/pulsectl.py @@ -2,6 +2,8 @@ """Displays volume and mute status and controls for PulseAudio devices. Use wheel up and down to change volume, left click mutes, right click opens pavucontrol. +**Please prefer this module over the "pulseaudio" module, which will eventually be deprecated + Aliases: pulseout (for outputs, such as headsets, speakers), pulsein (for microphones) NOTE: Do **not** use this module directly, but rather use either pulseout or pulsein!