diff --git a/README.md b/README.md index ffca800..153b412 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,11 @@ A collection of standalone python scripts for the slightly more complicated thin * call `cpu.py 1` to get the avg from last min * call `cpu.py 5` to get the avg from last 5 min * call `cpu.py 15` to get the avg from last 15 min +* **volume.py**: Now compatible with *pipewire*, parses output of `pactl` to output activ sink and current volume to i3blocks and manages music volume hotkeys. + * call `volume.py` without args to output status for i3blocks + * call `volume.py vol_up` to increase volume by 5% + * call `volume.py vol_down` to decrease volume by 5% + * call `volume.py mute` to toggle mute ## i3block_shell diff --git a/i3block_py/volume.py b/i3block_py/volume.py new file mode 100755 index 0000000..7008131 --- /dev/null +++ b/i3block_py/volume.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +""" +handler for media keys +integrated with pipewire by parsing pactl +""" + +import subprocess +import sys +from time import sleep + +# signal id to update i3blocks +I3BLOCKS_SIGNAL_ID = 10 + + +def get_status(): + """ returns the activ sink pased in a dict """ + # get list of sinks + state = subprocess.run(["pactl", "list", "sinks"], capture_output=True) + state_str = state.stdout.decode() + sinks = state_str.split('\n\n') + # loop through sinks + sinks_dict_list = [] + for sink in sinks: + sink_lines = sink.split('\n') + # build sink dict + sink_dict = {} + for i in sink_lines: + if ':' in i: + key, value = i.split(':', maxsplit=1) + elif '=' in i: + key, value = i.split('=') + else: + continue + key = key.strip().lower() + value = value.strip().strip('"') + if value: + sink_dict[key] = value + # append to list + sinks_dict_list.append(sink_dict) + # find active + if len(sinks_dict_list) == 1: + activ_sink = sinks_dict_list[0] + else: + for sink_dict in sinks_dict_list: + sink_name = sink_dict['name'] + if 'bluez' in sink_name: + activ_sink = sink_dict + break + # return activ + return activ_sink + + +def get_vol(activ_sink): + """ get the current volume level of activ sink """ + vol_string = activ_sink['volume'] + vol_list = [i for i in vol_string.split(' / ') if '%' in i] + vol = int(vol_list[0].strip().strip('%')) + return vol + + +def print_status(activ_sink): + """ print status for i3 blocks """ + mute = activ_sink['mute'] + # based on sink + if 'bluez' in activ_sink['name']: + # bluetooth + icon = '' + elif activ_sink['active port'] == 'analog-output-headphones': + # headphones + icon = '' + elif activ_sink['active port'] == 'analog-output-speaker': + # speaker + if mute == 'no': + icon = '' + else: + icon = '' + # color + if mute == 'no': + color = "#ffffff" + vol = str(get_vol(activ_sink)) + '%' + else: + color = "#404040" + vol = "MUTE" + # print three lines + print(f'{icon} {vol}') + print(f'{icon}') + print(color) + + +def mute(activ_sink): + """ toggle mute of activ sink """ + sink_id = activ_sink['object.id'] + subprocess.call(['pactl', 'set-sink-mute', sink_id, 'toggle']) + + +def vol_up(activ_sink): + """ increase vol in 5% increments """ + sink_id = activ_sink['object.id'] + vol = get_vol(activ_sink) + # if uneven vol level + if vol % 5 == 0: + target_vol = vol + 5 + else: + target_vol = vol + 5 - (vol % 5) + # how many times to loop + iterations = target_vol - vol + for _ in range(iterations): + subprocess.call(['pactl', 'set-sink-volume', sink_id, '+1%']) + sleep(0.1) + + +def vol_down(activ_sink): + """ decrese vol in 5% increments """ + sink_id = activ_sink['object.id'] + vol = get_vol(activ_sink) + # if uneven vol level + if vol % 5 == 0: + target_vol = vol - 5 + else: + target_vol = vol - (vol % 5) + # how many times to loop + iterations = vol - target_vol + for _ in range(iterations): + subprocess.call(['pactl', 'set-sink-volume', sink_id, '-1%']) + sleep(0.1) + + +def main(): + """ main to run """ + # parse args + args = sys.argv + activ_sink = get_status() + # if args + if len(args) > 1: + arg = args[1] + if arg == 'vol_up': + vol_up(activ_sink) + elif arg == 'vol_down': + vol_down(activ_sink) + elif arg == 'mute': + mute(activ_sink) + activ_sink = get_status() + subprocess.call(['pkill', '-RTMIN+' + str(I3BLOCKS_SIGNAL_ID), 'i3blocks']) + # status + print_status(activ_sink) + + +if __name__ == '__main__': + main()