2020-02-23 13:45:42 +01:00
# pylint: disable=C0111,R0903
2020-05-03 11:15:52 +02:00
""" 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.
2020-02-23 13:45:42 +01:00
Aliases : pasink ( use this to control output instead of input ) , pasource
Parameters :
* pulseaudio . autostart : If set to ' true ' ( default is ' false ' ) , automatically starts the pulseaudio daemon if it is not running
* pulseaudio . percent_change : How much to change volume by when scrolling on the module ( default is 2 % )
* pulseaudio . limit : Upper limit for setting the volume ( default is 0 % , which means ' no limit ' )
2020-05-06 12:57:38 +02:00
Note : If the left and right channels have different volumes , the limit might not be reached exactly .
2020-02-23 13:45:42 +01:00
* pulseaudio . showbars : 1 for showing volume bars , requires - - markup = pango ;
2020-05-06 12:57:38 +02:00
0 for not showing volume bars ( default )
2022-03-24 21:19:52 +01:00
* pulseaudio . 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 : pulseaudio . 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 :
pulseaudio . 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 : pulseaudio . left - click = select_default_device_popup
2020-02-23 13:45:42 +01:00
Requires the following executable :
* pulseaudio
* pactl
* pavucontrol
2020-05-03 11:15:52 +02:00
"""
2020-02-23 13:45:42 +01:00
import re
2022-08-30 21:38:48 +02:00
import os
2020-02-23 13:45:42 +01:00
import logging
2022-03-24 21:19:52 +01:00
import functools
2022-08-30 21:38:48 +02:00
import threading
import subprocess
import select
2020-02-23 13:45:42 +01:00
import core . module
import core . widget
import core . input
2022-08-30 21:38:48 +02:00
import core . event
2020-02-23 13:45:42 +01:00
import util . cli
2020-02-23 13:59:47 +01:00
import util . graph
import util . format
2020-02-23 13:45:42 +01:00
2022-03-25 19:29:37 +01:00
try :
import util . popup
except ImportError as e :
logging . warning ( " Couldn ' t import util.popup: %s . Popups won ' t work! " , e )
2020-05-03 11:15:52 +02:00
2020-02-23 13:45:42 +01:00
class Module ( core . module . Module ) :
2020-04-26 16:39:24 +02:00
def __init__ ( self , config , theme , channel ) :
2022-03-24 21:19:52 +01:00
super ( ) . __init__ ( config , theme , core . widget . Widget ( self . display ) )
2020-02-23 13:45:42 +01:00
2020-05-03 11:15:52 +02:00
if util . format . asbool ( self . parameter ( " autostart " , False ) ) :
util . cli . execute ( " pulseaudio --start " , ignore_errors = True )
2020-02-23 13:45:42 +01:00
2020-05-03 11:15:52 +02:00
self . _change = util . format . asint (
self . parameter ( " percent_change " , " 2 % " ) . strip ( " % " ) , 0 , 100
)
self . _limit = util . format . asint ( self . parameter ( " limit " , " 0 % " ) . strip ( " % " ) , 0 )
2020-02-23 13:45:42 +01:00
self . _left = 0
self . _right = 0
self . _mono = 0
self . _mute = False
self . _failed = False
self . _channel = channel
2022-03-24 21:19:52 +01:00
self . __selected_default_device = None
2020-05-03 11:15:52 +02:00
self . _showbars = util . format . asbool ( self . parameter ( " showbars " , 0 ) )
2022-03-24 21:19:52 +01:00
self . __show_device_name = util . format . asbool (
self . parameter ( " showdevicename " , False )
)
2020-02-23 13:45:42 +01:00
self . _patterns = [
2020-05-03 11:15:52 +02:00
{ " expr " : " Name: " , " callback " : ( lambda line : False ) } ,
{
" expr " : " Mute: " ,
" callback " : (
lambda line : self . mute ( False if " no " in line . lower ( ) else True )
) ,
} ,
{ " expr " : " Volume: " , " callback " : self . getvolume } ,
2020-02-23 13:45:42 +01:00
]
2020-05-03 11:15:52 +02:00
core . input . register ( self , button = core . input . RIGHT_MOUSE , cmd = " pavucontrol " )
2020-02-23 13:45:42 +01:00
events = [
2020-05-03 11:15:52 +02:00
{ " type " : " mute " , " action " : self . toggle , " 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 ,
} ,
2020-02-23 13:45:42 +01:00
]
for event in events :
2020-05-03 11:15:52 +02:00
core . input . register ( self , button = event [ " button " ] , cmd = event [ " action " ] )
2020-02-23 13:45:42 +01:00
2022-08-30 21:38:48 +02:00
self . __monitor = threading . Thread ( target = self . __subscribe , args = ( ) )
self . __monitor . start ( )
def __subscribe ( self ) :
self . update2 ( )
2022-09-01 19:00:06 +02:00
core . event . trigger ( " update " , [ self . id ] , redraw_only = True )
2022-08-30 21:38:48 +02:00
try :
proc = subprocess . Popen ( " pactl subscribe " ,
stdout = subprocess . PIPE ,
2022-08-31 19:05:08 +02:00
stderr = subprocess . STDOUT ,
2022-08-30 21:38:48 +02:00
shell = True
)
except :
return
while threading . main_thread ( ) . is_alive ( ) :
r , w , e = select . select ( [ proc . stdout ] , [ ] , [ ] , 1 )
if not ( r or w or e ) :
continue # timeout
2022-08-31 19:05:08 +02:00
# whateve we got, use it
self . update2 ( )
core . event . trigger ( " update " , [ self . id ] , redraw_only = True )
core . event . trigger ( " draw " )
os . set_blocking ( proc . stdout . fileno ( ) , False )
proc . stdout . read ( )
os . set_blocking ( proc . stdout . fileno ( ) , True )
2022-08-30 21:38:48 +02:00
2020-02-23 13:45:42 +01:00
def set_volume ( self , amount ) :
2020-05-03 11:15:52 +02:00
util . cli . execute (
" pactl set- {} - {} @DEFAULT_ {} @ {} " . format (
self . _channel , " volume " , self . _channel . upper ( ) , amount
)
)
2020-02-23 13:45:42 +01:00
def increase_volume ( self , event ) :
2020-05-03 11:15:52 +02:00
if self . _limit > 0 : # we need to check the limit
2020-02-23 13:45:42 +01:00
left = int ( self . _left )
right = int ( self . _right )
2020-05-03 11:15:52 +02:00
if (
left + self . _change > = self . _limit
or right + self . _change > = self . _limit
) :
2020-02-23 13:45:42 +01:00
if left == right :
# easy case, just set to limit
2020-05-03 11:15:52 +02:00
self . set_volume ( " {} % " . format ( self . _limit ) )
2020-02-23 13:45:42 +01:00
return
else :
# don't adjust anymore, since i don't know how to update only one channel
return
2020-05-03 11:15:52 +02:00
self . set_volume ( " + {} % " . format ( self . _change ) )
2020-02-23 13:45:42 +01:00
def decrease_volume ( self , event ) :
2020-05-03 11:15:52 +02:00
self . set_volume ( " - {} % " . format ( self . _change ) )
2020-02-23 13:45:42 +01:00
def toggle ( self , event ) :
2020-05-03 11:15:52 +02:00
util . cli . execute (
" pactl set- {} -mute @DEFAULT_ {} @ toggle " . format (
self . _channel , self . _channel . upper ( )
)
)
2020-02-23 13:45:42 +01:00
def mute ( self , value ) :
self . _mute = value
def getvolume ( self , line ) :
2020-05-03 11:15:52 +02:00
if " mono " in line :
m = re . search ( r " mono:.* \ s* \ / \ s*( \ d+) % " , line )
2020-02-23 13:45:42 +01:00
if m :
self . _mono = m . group ( 1 )
2022-07-25 09:12:42 +02:00
self . _left = 0
self . _right = 0
2020-02-23 13:45:42 +01:00
else :
2020-05-03 11:15:52 +02:00
m = re . search ( r " left:.* \ s* \ / \ s*( \ d+) %.*r ight:.* \ s* \ / \ s*( \ d+) % " , line )
2020-02-23 13:45:42 +01:00
if m :
2022-07-25 09:12:42 +02:00
self . _mono = 0
2020-02-23 13:45:42 +01:00
self . _left = m . group ( 1 )
self . _right = m . group ( 2 )
def _default_device ( self ) :
2020-05-03 11:15:52 +02:00
output = util . cli . execute ( " pactl info " )
pattern = " Default {} : " . format ( " Sink " if self . _channel == " sink " else " Source " )
for line in output . split ( " \n " ) :
2020-02-23 13:45:42 +01:00
if line . startswith ( pattern ) :
2020-05-03 11:15:52 +02:00
return line . replace ( pattern , " " )
logging . error ( " no pulseaudio device found " )
return " n/a "
2020-02-23 13:45:42 +01:00
2022-03-24 21:19:52 +01:00
def display ( self , widget ) :
2020-02-23 13:45:42 +01:00
if self . _failed == True :
2020-05-03 11:15:52 +02:00
return " n/a "
2022-03-24 21:19:52 +01:00
vol = None
2020-02-23 13:45:42 +01:00
if int ( self . _mono ) > 0 :
2020-05-03 11:15:52 +02:00
vol = " {} % " . format ( self . _mono )
2020-02-23 13:59:47 +01:00
if self . _showbars :
2020-05-03 11:15:52 +02:00
vol = " {} {} " . format ( vol , util . graph . hbar ( float ( self . _mono ) ) )
2020-02-23 13:45:42 +01:00
elif self . _left == self . _right :
2020-05-03 11:15:52 +02:00
vol = " {} % " . format ( self . _left )
2020-02-23 13:59:47 +01:00
if self . _showbars :
2020-05-03 11:15:52 +02:00
vol = " {} {} " . format ( vol , util . graph . hbar ( float ( self . _left ) ) )
2020-02-23 13:45:42 +01:00
else :
2020-05-03 11:15:52 +02:00
vol = " {} % / {} % " . format ( self . _left , self . _right )
2020-02-23 13:59:47 +01:00
if self . _showbars :
2020-05-03 11:15:52 +02:00
vol = " {} {} {} " . format (
2020-02-23 13:59:47 +01:00
vol ,
util . graph . hbar ( float ( self . _left ) ) ,
2020-05-03 11:15:52 +02:00
util . graph . hbar ( float ( self . _right ) ) ,
)
2022-03-24 21:19:52 +01:00
output = vol
if self . __show_device_name :
friendly_name = self . parameter (
self . __selected_default_device , self . __selected_default_device
)
icon = self . parameter ( " icon. " + self . __selected_default_device , " " )
output = (
icon + " " + friendly_name + " | " + vol
if icon != " "
else friendly_name + " | " + vol
)
return output
2020-02-23 13:45:42 +01:00
def update ( self ) :
2022-08-30 21:38:48 +02:00
if self . __monitor . is_alive ( ) :
return
self . update2 ( )
def update2 ( self ) :
2020-02-23 13:45:42 +01:00
try :
self . _failed = False
2020-05-03 11:15:52 +02:00
channel = " sinks " if self . _channel == " sink " else " sources "
2022-03-24 21:19:52 +01:00
self . __selected_default_device = self . _default_device ( )
2020-02-23 13:45:42 +01:00
2020-05-03 11:15:52 +02:00
result = util . cli . execute ( " pactl list {} " . format ( channel ) )
2020-02-23 13:45:42 +01:00
found = False
2020-05-03 11:15:52 +02:00
for line in result . split ( " \n " ) :
2022-03-24 21:19:52 +01:00
if " Name: {} " . format ( self . __selected_default_device ) in line :
2020-02-23 13:45:42 +01:00
found = True
continue
if found is False :
continue
for pattern in self . _patterns :
2020-05-03 11:15:52 +02:00
if not pattern [ " expr " ] in line :
2020-02-23 13:45:42 +01:00
continue
2020-05-03 11:15:52 +02:00
if pattern [ " callback " ] ( line ) is False and found == True :
2020-02-23 13:45:42 +01:00
return
except Exception as e :
self . _failed = True
logging . exception ( e )
2020-05-03 11:15:52 +02:00
if util . format . asbool ( self . parameter ( " autostart " , False ) ) :
util . cli . execute ( " pulseaudio --start " , ignore_errors = True )
2020-03-01 14:09:45 +01:00
else :
raise e
2020-02-23 13:45:42 +01:00
2022-03-24 21:19:52 +01:00
def __on_sink_selected ( self , sink_name ) :
util . cli . execute ( " pactl set-default- {} {} " . format ( self . _channel , sink_name ) )
def select_default_device_popup ( self , widget ) :
channel = " sinks " if self . _channel == " sink " else " sources "
result = util . cli . execute ( " pactl list {} short " . format ( channel ) )
menu = util . popup . menu ( )
lines = result . splitlines ( )
for line in lines :
info = line . split ( " \t " )
try :
friendly_name = self . parameter ( info [ 1 ] , info [ 1 ] )
menu . add_menuitem (
friendly_name ,
callback = functools . partial ( self . __on_sink_selected , info [ 1 ] ) ,
)
except :
logging . exception ( " Couldn ' t parse {} " . format ( channel ) )
pass
menu . show ( widget )
2020-02-23 13:45:42 +01:00
def state ( self , widget ) :
if self . _mute :
2020-05-03 11:15:52 +02:00
return [ " warning " , " muted " ]
return [ " unmuted " ]
2020-02-23 13:45:42 +01:00
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4