1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056 |
- #!/usr/bin/env python3
- # this script is taken from https://github.com/GeorgeFilipkin/pulsemixer
- '''Usage of pulsemixer:
- -h, --help show this help message and exit
- -v, --version print version
- -l, --list list everything
- --list-sources list sources
- --list-sinks list sinks
- --id ID specify ID, default sink is used if no ID specified
- --get-volume get volume for ID
- --set-volume n set volume for ID
- --set-volume-all n:n set volume for ID, for every channel
- --change-volume +-n change volume for ID
- --max-volume n set volume to n if volume is higher than n
- --get-mute get mute for ID
- --mute mute ID
- --unmute unmute ID
- --toggle-mute toggle mute for ID
- --server choose the server to connect to
- --color n 0 no color, 1 color currently selected, 2 full-color
- --no-mouse disable mouse support
- --create-config generate configuration file'''
- VERSION = '1.5.1'
- import curses
- import functools
- import getopt
- import operator
- import os
- import re
- import signal
- import sys
- import threading
- import traceback
- from collections import OrderedDict
- from configparser import ConfigParser
- from ctypes import *
- from itertools import takewhile
- from pprint import pprint
- from select import select
- from shutil import get_terminal_size
- from textwrap import dedent
- from time import sleep
- from unicodedata import east_asian_width
- #########################################################################################
- # v bindings
- try:
- DLL = CDLL("libpulse.so.0")
- except Exception as e:
- sys.exit(e)
- PA_VOLUME_NORM = 65536
- PA_CHANNELS_MAX = 32
- PA_USEC_T = c_uint64
- PA_CONTEXT_READY = 4
- PA_CONTEXT_FAILED = 5
- PA_SUBSCRIPTION_MASK_ALL = 0x02ff
- class Struct(Structure): pass
- PA_PROPLIST = PA_OPERATION = PA_CONTEXT = PA_THREADED_MAINLOOP = PA_MAINLOOP_API = Struct
- class PA_SAMPLE_SPEC(Structure):
- _fields_ = [
- ("format", c_int),
- ("rate", c_uint32),
- ("channels", c_uint32)
- ]
- class PA_CHANNEL_MAP(Structure):
- _fields_ = [
- ("channels", c_uint8),
- ("map", c_int * PA_CHANNELS_MAX)
- ]
- class PA_CVOLUME(Structure):
- _fields_ = [
- ("channels", c_uint8),
- ("values", c_uint32 * PA_CHANNELS_MAX)
- ]
- class PA_PORT_INFO(Structure):
- _fields_ = [
- ('name', c_char_p),
- ('description', c_char_p),
- ('priority', c_uint32),
- ("available", c_int),
- ]
- class PA_SINK_INPUT_INFO(Structure):
- _fields_ = [
- ("index", c_uint32),
- ("name", c_char_p),
- ("owner_module", c_uint32),
- ("client", c_uint32),
- ("sink", c_uint32),
- ("sample_spec", PA_SAMPLE_SPEC),
- ("channel_map", PA_CHANNEL_MAP),
- ("volume", PA_CVOLUME),
- ("buffer_usec", PA_USEC_T),
- ("sink_usec", PA_USEC_T),
- ("resample_method", c_char_p),
- ("driver", c_char_p),
- ("mute", c_int),
- ("proplist", POINTER(PA_PROPLIST))
- ]
- class PA_SINK_INFO(Structure):
- _fields_ = [
- ("name", c_char_p),
- ("index", c_uint32),
- ("description", c_char_p),
- ("sample_spec", PA_SAMPLE_SPEC),
- ("channel_map", PA_CHANNEL_MAP),
- ("owner_module", c_uint32),
- ("volume", PA_CVOLUME),
- ("mute", c_int),
- ("monitor_source", c_uint32),
- ("monitor_source_name", c_char_p),
- ("latency", PA_USEC_T),
- ("driver", c_char_p),
- ("flags", c_int),
- ("proplist", POINTER(PA_PROPLIST)),
- ("configured_latency", PA_USEC_T),
- ('base_volume', c_int),
- ('state', c_int),
- ('n_volume_steps', c_int),
- ('card', c_uint32),
- ('n_ports', c_uint32),
- ('ports', POINTER(POINTER(PA_PORT_INFO))),
- ('active_port', POINTER(PA_PORT_INFO))
- ]
- class PA_SOURCE_OUTPUT_INFO(Structure):
- _fields_ = [
- ("index", c_uint32),
- ("name", c_char_p),
- ("owner_module", c_uint32),
- ("client", c_uint32),
- ("source", c_uint32),
- ("sample_spec", PA_SAMPLE_SPEC),
- ("channel_map", PA_CHANNEL_MAP),
- ("buffer_usec", PA_USEC_T),
- ("source_usec", PA_USEC_T),
- ("resample_method", c_char_p),
- ("driver", c_char_p),
- ("proplist", POINTER(PA_PROPLIST)),
- ("corked", c_int),
- ("volume", PA_CVOLUME),
- ("mute", c_int),
- ]
- class PA_SOURCE_INFO(Structure):
- _fields_ = [
- ("name", c_char_p),
- ("index", c_uint32),
- ("description", c_char_p),
- ("sample_spec", PA_SAMPLE_SPEC),
- ("channel_map", PA_CHANNEL_MAP),
- ("owner_module", c_uint32),
- ("volume", PA_CVOLUME),
- ("mute", c_int),
- ("monitor_of_sink", c_uint32),
- ("monitor_of_sink_name", c_char_p),
- ("latency", PA_USEC_T),
- ("driver", c_char_p),
- ("flags", c_int),
- ("proplist", POINTER(PA_PROPLIST)),
- ("configured_latency", PA_USEC_T),
- ('base_volume', c_int),
- ('state', c_int),
- ('n_volume_steps', c_int),
- ('card', c_uint32),
- ('n_ports', c_uint32),
- ('ports', POINTER(POINTER(PA_PORT_INFO))),
- ('active_port', POINTER(PA_PORT_INFO))
- ]
- class PA_CLIENT_INFO(Structure):
- _fields_ = [
- ("index", c_uint32),
- ("name", c_char_p),
- ("owner_module", c_uint32),
- ("driver", c_char_p)
- ]
- class PA_CARD_PROFILE_INFO(Structure):
- _fields_ = [
- ('name', c_char_p),
- ('description', c_char_p),
- ('n_sinks', c_uint32),
- ('n_sources', c_uint32),
- ('priority', c_uint32),
- ]
- class PA_CARD_PROFILE_INFO2(Structure):
- _fields_ = PA_CARD_PROFILE_INFO._fields_ + [('available', c_int)]
- class PA_CARD_INFO(Structure):
- _fields_ = [
- ('index', c_uint32),
- ('name', c_char_p),
- ('owner_module', c_uint32),
- ('driver', c_char_p),
- ('n_profiles', c_uint32),
- ('profiles', POINTER(PA_CARD_PROFILE_INFO)),
- ('active_profile', POINTER(PA_CARD_PROFILE_INFO)),
- ('proplist', POINTER(PA_PROPLIST)),
- ('n_ports', c_uint32),
- ('ports', POINTER(POINTER(c_void_p))),
- ('profiles2', POINTER(POINTER(PA_CARD_PROFILE_INFO2))),
- ('active_profile2', POINTER(PA_CARD_PROFILE_INFO2))
- ]
- class PA_SERVER_INFO(Structure):
- _fields_ = [
- ('user_name', c_char_p),
- ('host_name', c_char_p),
- ('server_version', c_char_p),
- ('server_name', c_char_p),
- ('sample_spec', PA_SAMPLE_SPEC),
- ('default_sink_name', c_char_p),
- ('default_source_name', c_char_p),
- ]
- PA_STATE_CB_T = CFUNCTYPE(c_int, POINTER(PA_CONTEXT), c_void_p)
- PA_CLIENT_INFO_CB_T = CFUNCTYPE(c_void_p, POINTER(PA_CONTEXT), POINTER(PA_CLIENT_INFO), c_int, c_void_p)
- PA_SINK_INPUT_INFO_CB_T = CFUNCTYPE(c_int, POINTER(PA_CONTEXT), POINTER(PA_SINK_INPUT_INFO), c_int, c_void_p)
- PA_SINK_INFO_CB_T = CFUNCTYPE(c_int, POINTER(PA_CONTEXT), POINTER(PA_SINK_INFO), c_int, c_void_p)
- PA_SOURCE_OUTPUT_INFO_CB_T = CFUNCTYPE(c_int, POINTER(PA_CONTEXT), POINTER(PA_SOURCE_OUTPUT_INFO), c_int, c_void_p)
- PA_SOURCE_INFO_CB_T = CFUNCTYPE(c_int, POINTER(PA_CONTEXT), POINTER(PA_SOURCE_INFO), c_int, c_void_p)
- PA_CONTEXT_SUCCESS_CB_T = CFUNCTYPE(c_void_p, POINTER(PA_CONTEXT), c_int, c_void_p)
- PA_CARD_INFO_CB_T = CFUNCTYPE(None, POINTER(PA_CONTEXT), POINTER(PA_CARD_INFO), c_int, c_void_p)
- PA_SERVER_INFO_CB_T = CFUNCTYPE(None, POINTER(PA_CONTEXT), POINTER(PA_SERVER_INFO), c_void_p)
- PA_CONTEXT_SUBSCRIBE_CB_T = CFUNCTYPE(c_void_p, POINTER(PA_CONTEXT), c_int, c_int, c_void_p)
- pa_threaded_mainloop_new = DLL.pa_threaded_mainloop_new
- pa_threaded_mainloop_new.restype = POINTER(PA_THREADED_MAINLOOP)
- pa_threaded_mainloop_new.argtypes = []
- pa_threaded_mainloop_free = DLL.pa_threaded_mainloop_free
- pa_threaded_mainloop_free.restype = c_void_p
- pa_threaded_mainloop_free.argtypes = [POINTER(PA_THREADED_MAINLOOP)]
- pa_threaded_mainloop_start = DLL.pa_threaded_mainloop_start
- pa_threaded_mainloop_start.restype = c_int
- pa_threaded_mainloop_start.argtypes = [POINTER(PA_THREADED_MAINLOOP)]
- pa_threaded_mainloop_stop = DLL.pa_threaded_mainloop_stop
- pa_threaded_mainloop_stop.restype = None
- pa_threaded_mainloop_stop.argtypes = [POINTER(PA_THREADED_MAINLOOP)]
- pa_threaded_mainloop_lock = DLL.pa_threaded_mainloop_lock
- pa_threaded_mainloop_lock.restype = None
- pa_threaded_mainloop_lock.argtypes = [POINTER(PA_THREADED_MAINLOOP)]
- pa_threaded_mainloop_unlock = DLL.pa_threaded_mainloop_unlock
- pa_threaded_mainloop_unlock.restype = None
- pa_threaded_mainloop_unlock.argtypes = [POINTER(PA_THREADED_MAINLOOP)]
- pa_threaded_mainloop_wait = DLL.pa_threaded_mainloop_wait
- pa_threaded_mainloop_wait.restype = None
- pa_threaded_mainloop_wait.argtypes = [POINTER(PA_THREADED_MAINLOOP)]
- pa_threaded_mainloop_signal = DLL.pa_threaded_mainloop_signal
- pa_threaded_mainloop_signal.restype = None
- pa_threaded_mainloop_signal.argtypes = [POINTER(PA_THREADED_MAINLOOP), c_int]
- pa_threaded_mainloop_get_api = DLL.pa_threaded_mainloop_get_api
- pa_threaded_mainloop_get_api.restype = POINTER(PA_MAINLOOP_API)
- pa_threaded_mainloop_get_api.argtypes = [POINTER(PA_THREADED_MAINLOOP)]
- pa_context_errno = DLL.pa_context_errno
- pa_context_errno.restype = c_int
- pa_context_errno.argtypes = [POINTER(PA_CONTEXT)]
- pa_context_new_with_proplist = DLL.pa_context_new_with_proplist
- pa_context_new_with_proplist.restype = POINTER(PA_CONTEXT)
- pa_context_new_with_proplist.argtypes = [POINTER(PA_MAINLOOP_API), c_char_p, POINTER(PA_PROPLIST)]
- pa_context_unref = DLL.pa_context_unref
- pa_context_unref.restype = None
- pa_context_unref.argtypes = [POINTER(PA_CONTEXT)]
- pa_context_set_state_callback = DLL.pa_context_set_state_callback
- pa_context_set_state_callback.restype = None
- pa_context_set_state_callback.argtypes = [POINTER(PA_CONTEXT), PA_STATE_CB_T, c_void_p]
- pa_context_connect = DLL.pa_context_connect
- pa_context_connect.restype = c_int
- pa_context_connect.argtypes = [POINTER(PA_CONTEXT), c_char_p, c_int, POINTER(c_int)]
- pa_context_get_state = DLL.pa_context_get_state
- pa_context_get_state.restype = c_int
- pa_context_get_state.argtypes = [POINTER(PA_CONTEXT)]
- pa_context_disconnect = DLL.pa_context_disconnect
- pa_context_disconnect.restype = c_int
- pa_context_disconnect.argtypes = [POINTER(PA_CONTEXT)]
- pa_operation_unref = DLL.pa_operation_unref
- pa_operation_unref.restype = None
- pa_operation_unref.argtypes = [POINTER(PA_OPERATION)]
- pa_context_subscribe = DLL.pa_context_subscribe
- pa_context_subscribe.restype = POINTER(PA_OPERATION)
- pa_context_subscribe.argtypes = [POINTER(PA_CONTEXT), c_int, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
- pa_context_set_subscribe_callback = DLL.pa_context_set_subscribe_callback
- pa_context_set_subscribe_callback.restype = None
- pa_context_set_subscribe_callback.args = [POINTER(PA_CONTEXT), PA_CONTEXT_SUBSCRIBE_CB_T, c_void_p]
- pa_proplist_new = DLL.pa_proplist_new
- pa_proplist_new.restype = POINTER(PA_PROPLIST)
- pa_proplist_sets = DLL.pa_proplist_sets
- pa_proplist_sets.argtypes = [POINTER(PA_PROPLIST), c_char_p, c_char_p]
- pa_proplist_gets = DLL.pa_proplist_gets
- pa_proplist_gets.restype = c_char_p
- pa_proplist_gets.argtypes = [POINTER(PA_PROPLIST), c_char_p]
- pa_proplist_free = DLL.pa_proplist_free
- pa_proplist_free.argtypes = [POINTER(PA_PROPLIST)]
- pa_context_get_sink_input_info_list = DLL.pa_context_get_sink_input_info_list
- pa_context_get_sink_input_info_list.restype = POINTER(PA_OPERATION)
- pa_context_get_sink_input_info_list.argtypes = [POINTER(PA_CONTEXT), PA_SINK_INPUT_INFO_CB_T, c_void_p]
- pa_context_get_sink_info_list = DLL.pa_context_get_sink_info_list
- pa_context_get_sink_info_list.restype = POINTER(PA_OPERATION)
- pa_context_get_sink_info_list.argtypes = [POINTER(PA_CONTEXT), PA_SINK_INFO_CB_T, c_void_p]
- pa_context_set_sink_mute_by_index = DLL.pa_context_set_sink_mute_by_index
- pa_context_set_sink_mute_by_index.restype = POINTER(PA_OPERATION)
- pa_context_set_sink_mute_by_index.argtypes = [POINTER(PA_CONTEXT), c_uint32, c_int, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
- pa_context_suspend_sink_by_index = DLL.pa_context_suspend_sink_by_index
- pa_context_suspend_sink_by_index.restype = POINTER(PA_OPERATION)
- pa_context_suspend_sink_by_index.argtypes = [POINTER(PA_CONTEXT), c_uint32, c_int, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
- pa_context_set_sink_port_by_index = DLL.pa_context_set_sink_port_by_index
- pa_context_set_sink_port_by_index.restype = POINTER(PA_OPERATION)
- pa_context_set_sink_port_by_index.argtypes = [POINTER(PA_CONTEXT), c_uint32, c_char_p, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
- pa_context_set_sink_input_mute = DLL.pa_context_set_sink_input_mute
- pa_context_set_sink_input_mute.restype = POINTER(PA_OPERATION)
- pa_context_set_sink_input_mute.argtypes = [POINTER(PA_CONTEXT), c_uint32, c_int, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
- pa_context_set_sink_volume_by_index = DLL.pa_context_set_sink_volume_by_index
- pa_context_set_sink_volume_by_index.restype = POINTER(PA_OPERATION)
- pa_context_set_sink_volume_by_index.argtypes = [POINTER(PA_CONTEXT), c_uint32, POINTER(PA_CVOLUME), PA_CONTEXT_SUCCESS_CB_T, c_void_p]
- pa_context_set_sink_input_volume = DLL.pa_context_set_sink_input_volume
- pa_context_set_sink_input_volume.restype = POINTER(PA_OPERATION)
- pa_context_set_sink_input_volume.argtypes = [POINTER(PA_CONTEXT), c_uint32, POINTER(PA_CVOLUME), PA_CONTEXT_SUCCESS_CB_T, c_void_p]
- pa_context_move_sink_input_by_index = DLL.pa_context_move_sink_input_by_index
- pa_context_move_sink_input_by_index.restype = POINTER(PA_OPERATION)
- pa_context_move_sink_input_by_index.argtypes = [POINTER(PA_CONTEXT), c_uint32, c_uint32, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
- pa_context_set_default_sink = DLL.pa_context_set_default_sink
- pa_context_set_default_sink.restype = POINTER(PA_OPERATION)
- pa_context_set_default_sink.argtypes = [POINTER(PA_CONTEXT), c_char_p, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
- pa_context_kill_sink_input = DLL.pa_context_kill_sink_input
- pa_context_kill_sink_input.restype = POINTER(PA_OPERATION)
- pa_context_kill_sink_input.argtypes = [POINTER(PA_CONTEXT), c_uint32, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
- pa_context_kill_client = DLL.pa_context_kill_client
- pa_context_kill_client.restype = POINTER(PA_OPERATION)
- pa_context_kill_client.argtypes = [POINTER(PA_CONTEXT), c_uint32, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
- pa_context_get_source_output_info_list = DLL.pa_context_get_source_output_info_list
- pa_context_get_source_output_info_list.restype = POINTER(PA_OPERATION)
- pa_context_get_source_output_info_list.argtypes = [POINTER(PA_CONTEXT), PA_SOURCE_OUTPUT_INFO_CB_T, c_void_p]
- pa_context_move_source_output_by_index = DLL.pa_context_move_source_output_by_index
- pa_context_move_source_output_by_index.restype = POINTER(PA_OPERATION)
- pa_context_move_source_output_by_index.argtypes = [POINTER(PA_CONTEXT), c_uint32, c_uint32, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
- pa_context_set_source_output_volume = DLL.pa_context_set_source_output_volume
- pa_context_set_source_output_volume.restype = POINTER(PA_OPERATION)
- pa_context_set_source_output_volume.argtypes = [POINTER(PA_CONTEXT), c_uint32, POINTER(PA_CVOLUME), PA_CONTEXT_SUCCESS_CB_T, c_void_p]
- pa_context_set_source_output_mute = DLL.pa_context_set_source_output_mute
- pa_context_set_source_output_mute.restype = POINTER(PA_OPERATION)
- pa_context_set_source_output_mute.argtypes = [POINTER(PA_CONTEXT), c_uint32, c_int, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
- pa_context_get_source_info_list = DLL.pa_context_get_source_info_list
- pa_context_get_source_info_list.restype = POINTER(PA_OPERATION)
- pa_context_get_source_info_list.argtypes = [POINTER(PA_CONTEXT), PA_SOURCE_INFO_CB_T, c_void_p]
- pa_context_set_source_volume_by_index = DLL.pa_context_set_source_volume_by_index
- pa_context_set_source_volume_by_index.restype = POINTER(PA_OPERATION)
- pa_context_set_source_volume_by_index.argtypes = [POINTER(PA_CONTEXT), c_uint32, POINTER(PA_CVOLUME), PA_CONTEXT_SUCCESS_CB_T, c_void_p]
- pa_context_set_source_mute_by_index = DLL.pa_context_set_source_mute_by_index
- pa_context_set_source_mute_by_index.restype = POINTER(PA_OPERATION)
- pa_context_set_source_mute_by_index.argtypes = [POINTER(PA_CONTEXT), c_uint32, c_int, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
- pa_context_suspend_source_by_index = DLL.pa_context_suspend_source_by_index
- pa_context_suspend_source_by_index.restype = POINTER(PA_OPERATION)
- pa_context_suspend_source_by_index.argtypes = [POINTER(PA_CONTEXT), c_uint32, c_int, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
- pa_context_set_source_port_by_index = DLL.pa_context_set_source_port_by_index
- pa_context_set_source_port_by_index.restype = POINTER(PA_OPERATION)
- pa_context_set_source_port_by_index.argtypes = [POINTER(PA_CONTEXT), c_uint32, c_char_p, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
- pa_context_set_default_source = DLL.pa_context_set_default_source
- pa_context_set_default_source.restype = POINTER(PA_OPERATION)
- pa_context_set_default_source.argtypes = [POINTER(PA_CONTEXT), c_char_p, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
- pa_context_kill_source_output = DLL.pa_context_kill_source_output
- pa_context_kill_source_output.restype = POINTER(PA_OPERATION)
- pa_context_kill_source_output.argtypes = [POINTER(PA_CONTEXT), c_uint32, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
- pa_context_get_client_info_list = DLL.pa_context_get_client_info_list
- pa_context_get_client_info_list.restype = POINTER(PA_OPERATION)
- pa_context_get_client_info_list.argtypes = [POINTER(PA_CONTEXT), PA_CLIENT_INFO_CB_T, c_void_p]
- pa_context_get_card_info_list = DLL.pa_context_get_card_info_list
- pa_context_get_card_info_list.restype = POINTER(PA_OPERATION)
- pa_context_get_card_info_list.argtypes = [POINTER(PA_CONTEXT), PA_CARD_INFO_CB_T, c_void_p]
- pa_context_set_card_profile_by_index = DLL.pa_context_set_card_profile_by_index
- pa_context_set_card_profile_by_index.restype = POINTER(PA_OPERATION)
- pa_context_set_card_profile_by_index.argtypes = [POINTER(PA_CONTEXT), c_uint32, c_char_p, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
- pa_context_get_server_info = DLL.pa_context_get_server_info
- pa_context_get_server_info.restype = POINTER(PA_OPERATION)
- pa_context_get_server_info.argtypes = [POINTER(PA_CONTEXT), PA_SERVER_INFO_CB_T, c_void_p]
- pa_get_library_version = DLL.pa_get_library_version
- pa_get_library_version.restype = c_char_p
- PA_MAJOR = int(pa_get_library_version().decode().split('.')[0])
- # ^ bindings
- #########################################################################################
- # v lib
- class DebugMixin():
- def debug(self):
- pprint(vars(self))
- class PulsePort(DebugMixin):
- def __init__(self, pa_port):
- self.name = pa_port.name
- self.description = pa_port.description
- self.priority = pa_port.priority
- self.available = getattr(pa_port, "available", 0)
- if self.available == 1: # 1 off, 0 n/a, 2 on
- self.description += b' / off'
- class PulseServer(DebugMixin):
- def __init__(self, pa_server):
- self.default_sink_name = pa_server.default_sink_name
- self.default_source_name = pa_server.default_source_name
- self.server_version = pa_server.server_version
- class PulseCardProfile(DebugMixin):
- def __init__(self, pa_profile):
- self.name = pa_profile.name
- self.description = pa_profile.description
- self.available = getattr(pa_profile, "available", 1)
- if not self.available:
- self.description += b' / off'
- class PulseCard(DebugMixin):
- def __init__(self, pa_card):
- self.name = pa_card.name
- self.description = pa_proplist_gets(pa_card.proplist, b'device.description')
- self.index = pa_card.index
- self.driver = pa_card.driver
- self.owner_module = pa_card.owner_module
- self.n_profiles = pa_card.n_profiles
- if PA_MAJOR >= 5:
- self.profiles = [PulseCardProfile(pa_card.profiles2[n].contents) for n in range(self.n_profiles)]
- self.active_profile = PulseCardProfile(pa_card.active_profile2[0])
- else: # fallback to legacy profile, for PA < 5.0 (March 2014)
- self.profiles = [PulseCardProfile(pa_card.profiles[n]) for n in range(self.n_profiles)]
- self.active_profile = PulseCardProfile(pa_card.active_profile[0])
- self.volume = type('volume', (object, ), {'channels': 1, 'values': [0, 0]})
- def __str__(self):
- return "Card-ID: {}, Name: {}".format(self.index, self.name.decode())
- class PulseClient(DebugMixin):
- def __init__(self, pa_client):
- self.index = getattr(pa_client, "index", 0)
- self.name = getattr(pa_client, "name", pa_client)
- self.driver = getattr(pa_client, "driver", "default driver")
- self.owner_module = getattr(pa_client, "owner_module", -1)
- def __str__(self):
- return "Client-name: {}".format(self.name.decode())
- class Pulse(DebugMixin):
- def __init__(self, client_name='libpulse', server_name=None, reconnect=False):
- self.error = None
- self.data = []
- self.operation = None
- self.connected = False
- self.client_name = client_name.encode()
- self.server_name = server_name
- self.pa_state_cb = PA_STATE_CB_T(self.state_cb)
- self.pa_subscribe_cb = self.pa_dc_cb = lambda: None
- self.pa_cbs = {'sink_input_list': PA_SINK_INPUT_INFO_CB_T(self.sink_input_list_cb),
- 'source_output_list': PA_SOURCE_OUTPUT_INFO_CB_T(self.source_output_list_cb),
- 'sink_list': PA_SINK_INFO_CB_T(self.sink_list_cb),
- 'source_list': PA_SOURCE_INFO_CB_T(self.source_list_cb),
- 'server': PA_SERVER_INFO_CB_T(self.server_cb),
- 'card_list': PA_CARD_INFO_CB_T(self.card_list_cb),
- 'client_list': PA_CLIENT_INFO_CB_T(self.client_list_cb),
- 'success': PA_CONTEXT_SUCCESS_CB_T(self.context_success)}
- self.mainloop = pa_threaded_mainloop_new()
- self.mainloop_api = pa_threaded_mainloop_get_api(self.mainloop)
- proplist = pa_proplist_new()
- pa_proplist_sets(proplist, b'application.id', self.client_name)
- pa_proplist_sets(proplist, b'application.icon_name', b'audio-card')
- self.context = pa_context_new_with_proplist(self.mainloop_api, self.client_name, proplist)
- pa_context_set_state_callback(self.context, self.pa_state_cb, None)
- pa_proplist_free(proplist)
- if pa_context_connect(self.context, self.server_name, 0, None) < 0 or self.error:
- if not reconnect: sys.exit("Failed to connect to pulseaudio: Connection refused")
- else: return
- pa_threaded_mainloop_lock(self.mainloop)
- pa_threaded_mainloop_start(self.mainloop)
- if self.error and reconnect: return
- pa_threaded_mainloop_wait(self.mainloop) or pa_threaded_mainloop_unlock(self.mainloop)
- if self.error and reconnect: return
- elif self.error: sys.exit('Failed to connect to pulseaudio')
- self.connected = True
- def wait_and_unlock(self):
- pa_threaded_mainloop_wait(self.mainloop)
- pa_threaded_mainloop_unlock(self.mainloop)
- pa_operation_unref(self.operation)
- def reconnect(self):
- if self.context:
- pa_context_disconnect(self.context)
- pa_context_unref(self.context)
- if self.mainloop:
- pa_threaded_mainloop_stop(self.mainloop)
- pa_threaded_mainloop_free(self.mainloop)
- self.__init__(self.client_name.decode(), self.server_name, reconnect=True)
- def unmute_stream(self, obj):
- if type(obj) is PulseSinkInfo:
- self.sink_mute(obj.index, 0)
- elif type(obj) is PulseSinkInputInfo:
- self.sink_input_mute(obj.index, 0)
- elif type(obj) is PulseSourceInfo:
- self.source_mute(obj.index, 0)
- elif type(obj) is PulseSourceOutputInfo:
- self.source_output_mute(obj.index, 0)
- obj.mute = 0
- def mute_stream(self, obj):
- if type(obj) is PulseSinkInfo:
- self.sink_mute(obj.index, 1)
- elif type(obj) is PulseSinkInputInfo:
- self.sink_input_mute(obj.index, 1)
- elif type(obj) is PulseSourceInfo:
- self.source_mute(obj.index, 1)
- elif type(obj) is PulseSourceOutputInfo:
- self.source_output_mute(obj.index, 1)
- obj.mute = 1
- def set_volume(self, obj, volume):
- if type(obj) is PulseSinkInfo:
- self.set_sink_volume(obj.index, volume)
- elif type(obj) is PulseSinkInputInfo:
- self.set_sink_input_volume(obj.index, volume)
- elif type(obj) is PulseSourceInfo:
- self.set_source_volume(obj.index, volume)
- elif type(obj) is PulseSourceOutputInfo:
- self.set_source_output_volume(obj.index, volume)
- obj.volume = volume
- def change_volume_mono(self, obj, inc):
- obj.volume.values = [v + inc for v in obj.volume.values]
- self.set_volume(obj, obj.volume)
- def get_volume_mono(self, obj):
- return int(sum(obj.volume.values) / len(obj.volume.values))
- def fill_clients(self):
- if not self.data:
- return None
- data, self.data = self.data, []
- clist = self.client_list()
- for d in data:
- for c in clist:
- if c.index == d.client_id:
- d.client = c
- break
- return data
- def state_cb(self, c, b):
- state = pa_context_get_state(c)
- if state == PA_CONTEXT_READY:
- pa_threaded_mainloop_signal(self.mainloop, 0)
- elif state == PA_CONTEXT_FAILED:
- self.error = RuntimeError("Failed to complete action: {}, {}".format(state, pa_context_errno(c)))
- self.connected = False
- pa_threaded_mainloop_signal(self.mainloop, 0)
- self.pa_dc_cb()
- return 0
- def _eof_cb(func):
- def wrapper(self, c, info, eof, *args):
- if eof:
- pa_threaded_mainloop_signal(self.mainloop, 0)
- return 0
- func(self, c, info, eof, *args)
- return 0
- return wrapper
- def _action_sync(func):
- def wrapper(self, *args):
- if self.error: raise self.error
- pa_threaded_mainloop_lock(self.mainloop)
- try:
- func(self, *args)
- except Exception as e:
- pa_threaded_mainloop_unlock(self.mainloop)
- raise e
- self.wait_and_unlock()
- if func.__name__ in ('sink_input_list', 'source_output_list'):
- self.data = self.fill_clients()
- data, self.data = self.data, []
- return data or []
- return wrapper
- @_eof_cb
- def card_list_cb(self, c, card_info, eof, userdata):
- self.data.append(PulseCard(card_info[0]))
- @_eof_cb
- def client_list_cb(self, c, client_info, eof, userdata):
- self.data.append(PulseClient(client_info[0]))
- @_eof_cb
- def sink_input_list_cb(self, c, sink_input_info, eof, userdata):
- self.data.append(PulseSinkInputInfo(sink_input_info[0]))
- @_eof_cb
- def sink_list_cb(self, c, sink_info, eof, userdata):
- self.data.append(PulseSinkInfo(sink_info[0]))
- @_eof_cb
- def source_output_list_cb(self, c, source_output_info, eof, userdata):
- self.data.append(PulseSourceOutputInfo(source_output_info[0]))
- @_eof_cb
- def source_list_cb(self, c, source_info, eof, userdata):
- self.data.append(PulseSourceInfo(source_info[0]))
- def server_cb(self, c, server_info, userdata):
- self.data = PulseServer(server_info[0])
- pa_threaded_mainloop_signal(self.mainloop, 0)
- def context_success(self, *_):
- pa_threaded_mainloop_signal(self.mainloop, 0)
- def subscribe(self, cb):
- self.pa_subscribe_cb, self.pa_dc_cb = PA_CONTEXT_SUBSCRIBE_CB_T(cb), cb
- pa_context_set_subscribe_callback(self.context, self.pa_subscribe_cb, None)
- pa_threaded_mainloop_lock(self.mainloop)
- self.operation = pa_context_subscribe(self.context, PA_SUBSCRIPTION_MASK_ALL, self.pa_cbs['success'], None)
- self.wait_and_unlock()
- @_action_sync
- def sink_input_list(self):
- self.operation = pa_context_get_sink_input_info_list(self.context, self.pa_cbs['sink_input_list'], None)
- @_action_sync
- def source_output_list(self):
- self.operation = pa_context_get_source_output_info_list(self.context, self.pa_cbs['source_output_list'], None)
- @_action_sync
- def sink_list(self):
- self.operation = pa_context_get_sink_info_list(self.context, self.pa_cbs['sink_list'], None)
- @_action_sync
- def source_list(self):
- self.operation = pa_context_get_source_info_list(self.context, self.pa_cbs['source_list'], None)
- @_action_sync
- def get_server_info(self):
- self.operation = pa_context_get_server_info(self.context, self.pa_cbs['server'], None)
- @_action_sync
- def card_list(self):
- self.operation = pa_context_get_card_info_list(self.context, self.pa_cbs['card_list'], None)
- @_action_sync
- def client_list(self):
- self.operation = pa_context_get_client_info_list(self.context, self.pa_cbs['client_list'], None)
- @_action_sync
- def sink_input_mute(self, index, mute):
- self.operation = pa_context_set_sink_input_mute(self.context, index, mute, self.pa_cbs['success'], None)
- @_action_sync
- def sink_input_move(self, index, s_index):
- self.operation = pa_context_move_sink_input_by_index(self.context, index, s_index, self.pa_cbs['success'], None)
- @_action_sync
- def sink_mute(self, index, mute):
- self.operation = pa_context_set_sink_mute_by_index(self.context, index, mute, self.pa_cbs['success'], None)
- @_action_sync
- def set_sink_input_volume(self, index, vol):
- self.operation = pa_context_set_sink_input_volume(self.context, index, vol.to_c(), self.pa_cbs['success'], None)
- @_action_sync
- def set_sink_volume(self, index, vol):
- self.operation = pa_context_set_sink_volume_by_index(self.context, index, vol.to_c(), self.pa_cbs['success'], None)
- @_action_sync
- def sink_suspend(self, index, suspend):
- self.operation = pa_context_suspend_sink_by_index(self.context, index, suspend, self.pa_cbs['success'], None)
- @_action_sync
- def set_default_sink(self, name):
- self.operation = pa_context_set_default_sink(self.context, name, self.pa_cbs['success'], None)
- @_action_sync
- def kill_sink(self, index):
- self.operation = pa_context_kill_sink_input(self.context, index, self.pa_cbs['success'], None)
- @_action_sync
- def kill_client(self, index):
- self.operation = pa_context_kill_client(self.context, index, self.pa_cbs['success'], None)
- @_action_sync
- def set_sink_port(self, index, port):
- self.operation = pa_context_set_sink_port_by_index(self.context, index, port, self.pa_cbs['success'], None)
- @_action_sync
- def set_source_output_volume(self, index, vol):
- self.operation = pa_context_set_source_output_volume(self.context, index, vol.to_c(), self.pa_cbs['success'], None)
- @_action_sync
- def set_source_volume(self, index, vol):
- self.operation = pa_context_set_source_volume_by_index(self.context, index, vol.to_c(), self.pa_cbs['success'], None)
- @_action_sync
- def source_suspend(self, index, suspend):
- self.operation = pa_context_suspend_source_by_index(self.context, index, suspend, self.pa_cbs['success'], None)
- @_action_sync
- def set_default_source(self, name):
- self.operation = pa_context_set_default_source(self.context, name, self.pa_cbs['success'], None)
- @_action_sync
- def kill_source(self, index):
- self.operation = pa_context_kill_source_output(self.context, index, self.pa_cbs['success'], None)
- @_action_sync
- def set_source_port(self, index, port):
- self.operation = pa_context_set_source_port_by_index(self.context, index, port, self.pa_cbs['success'], None)
- @_action_sync
- def source_output_mute(self, index, mute):
- self.operation = pa_context_set_source_output_mute(self.context, index, mute, self.pa_cbs['success'], None)
- @_action_sync
- def source_mute(self, index, mute):
- self.operation = pa_context_set_source_mute_by_index(self.context, index, mute, self.pa_cbs['success'], None)
- @_action_sync
- def source_output_move(self, index, s_index):
- self.operation = pa_context_move_source_output_by_index(self.context, index, s_index, self.pa_cbs['success'], None)
- @_action_sync
- def set_card_profile(self, index, p_index):
- self.operation = pa_context_set_card_profile_by_index(self.context, index, p_index, self.pa_cbs['success'], None)
- class PulseSink(DebugMixin):
- def __init__(self, sink_info):
- self.index = sink_info.index
- self.name = sink_info.name
- self.mute = sink_info.mute
- self.volume = PulseVolume(sink_info.volume)
- class PulseSinkInfo(PulseSink):
- def __init__(self, pa_sink_info):
- PulseSink.__init__(self, pa_sink_info)
- self.description = pa_sink_info.description
- self.owner_module = pa_sink_info.owner_module
- self.driver = pa_sink_info.driver
- self.monitor_source = pa_sink_info.monitor_source
- self.monitor_source_name = pa_sink_info.monitor_source_name
- self.n_ports = pa_sink_info.n_ports
- self.ports = [PulsePort(pa_sink_info.ports[i].contents) for i in range(self.n_ports)]
- self.active_port = None
- if self.n_ports:
- self.active_port = PulsePort(pa_sink_info.active_port.contents)
- def __str__(self):
- return "ID: sink-{}, Name: {}, Mute: {}, {}".format(self.index, self.description.decode(), self.mute, self.volume)
- class PulseSinkInputInfo(PulseSink):
- def __init__(self, pa_sink_input_info):
- PulseSink.__init__(self, pa_sink_input_info)
- self.owner_module = pa_sink_input_info.owner_module
- self.client = PulseClient(pa_sink_input_info.name)
- self.client_id = pa_sink_input_info.client
- self.sink = self.owner = pa_sink_input_info.sink
- self.driver = pa_sink_input_info.driver
- self.media_name = pa_proplist_gets(pa_sink_input_info.proplist, b'media.name')
- def __str__(self):
- if self.client:
- return "ID: sink-input-{}, Name: {}, Mute: {}, {}".format(self.index, self.client.name.decode(), self.mute, self.volume)
- return "ID: sink-input-{}, Name: {}, Mute: {}".format(self.index, self.name.decode(), self.mute)
- class PulseSource(DebugMixin):
- def __init__(self, source_info):
- self.index = source_info.index
- self.name = source_info.name
- self.mute = source_info.mute
- self.volume = PulseVolume(source_info.volume)
- class PulseSourceInfo(PulseSource):
- def __init__(self, pa_source_info):
- PulseSource.__init__(self, pa_source_info)
- self.description = pa_source_info.description
- self.owner_module = pa_source_info.owner_module
- self.monitor_of_sink = pa_source_info.monitor_of_sink
- self.monitor_of_sink_name = pa_source_info.monitor_of_sink_name
- self.driver = pa_source_info.driver
- self.n_ports = pa_source_info.n_ports
- self.ports = [PulsePort(pa_source_info.ports[i].contents) for i in range(self.n_ports)]
- self.active_port = None
- if self.n_ports:
- self.active_port = PulsePort(pa_source_info.active_port.contents)
- def __str__(self):
- return "ID: source-{}, Name: {}, Mute: {}, {}".format(self.index, self.description.decode(), self.mute, self.volume)
- class PulseSourceOutputInfo(PulseSource):
- def __init__(self, pa_source_output_info):
- PulseSource.__init__(self, pa_source_output_info)
- self.owner_module = pa_source_output_info.owner_module
- self.client = PulseClient(pa_source_output_info.name)
- self.client_id = pa_source_output_info.client
- self.source = self.owner = pa_source_output_info.source
- self.driver = pa_source_output_info.driver
- self.application_id = pa_proplist_gets(pa_source_output_info.proplist, b'application.id')
- def __str__(self):
- if self.client:
- return "ID: source-output-{}, Name: {}, Mute: {}, {}".format(self.index, self.client.name.decode(), self.mute, self.volume)
- return "ID: source-output-{}, Name: {}, Mute: {}".format(self.index, self.name.decode(), self.mute)
- class PulseVolume(DebugMixin):
- def __init__(self, cvolume):
- self.channels = cvolume.channels
- self.values = [(round(x * 100 / PA_VOLUME_NORM)) for x in cvolume.values[:self.channels]]
- self.cvolume = PA_CVOLUME()
- self.cvolume.channels = self.channels
- def to_c(self):
- self.values = list(map(lambda x: max(min(x, 150), 0), self.values))
- for x in range(self.channels):
- self.cvolume.values[x] = round((self.values[x] * PA_VOLUME_NORM) / 100)
- return self.cvolume
- def __str__(self):
- return "Channels: {}, Volumes: {}".format(self.channels, [str(x) + "%" for x in self.values])
- # ^ lib
- #########################################################################################
- # v main
- class Bar():
- # should be in correct order
- LEFT, RIGHT, RLEFT, RRIGHT, CENTER, SUB, SLEFT, SRIGHT, NONE = range(9)
- def __init__(self, pa):
- if type(pa) is str:
- self.name = pa
- return
- if type(pa) in (PulseSinkInfo, PulseSourceInfo, PulseCard):
- self.fullname = pa.description.decode()
- else:
- self.fullname = pa.client.name.decode()
- self.name = re.sub(r'^ALSA plug-in \[|\]$', '', self.fullname.replace('|', ' '))
- for key in CFG.renames:
- if key.match(self.name):
- self.name = CFG.renames[key]
- break
- self.index = pa.index
- self.owner = -1
- self.stream_index = -1
- self.media_name, self.media_name_wide, self.media_name_widths = '', False, []
- self.poll_data(pa, 0, 0)
- self.maxsize = 150
- self.locked = True
- def poll_data(self, pa, owned, stream_index):
- self.channels = pa.volume.channels
- self.muted = getattr(pa, 'mute', False)
- self.owned = owned
- self.stream_index = stream_index
- self.volume = pa.volume.values
- if hasattr(pa, 'media_name'):
- media_fullname = pa.media_name.decode().replace('\n', ' ')
- media_name = ': {}'.format(media_fullname.replace('|', ' '))
- if media_fullname != self.fullname and media_name != self.media_name:
- self.media_name, self.media_name_wide = media_name, False
- if len(media_fullname) != len(pa.media_name): # contains multi-byte chars which might be wide
- self.media_name_widths = [int(east_asian_width(c) == 'W') + 1 for c in media_name]
- self.media_name_wide = 2 in self.media_name_widths
- else:
- self.media_name, self.media_name_wide = '', False
- if type(pa) in (PulseSinkInputInfo, PulseSourceOutputInfo):
- self.owner = pa.owner
- self.pa = pa
- def mute_toggle(self):
- PULSE.unmute_stream(self.pa) if self.muted else PULSE.mute_stream(self.pa)
- def lock_toggle(self):
- self.locked = not self.locked
- def set(self, n, side):
- vol = self.pa.volume
- if self.locked:
- for i, _ in enumerate(vol.values):
- vol.values[i] = n
- else:
- vol.values[side] = n
- PULSE.set_volume(self.pa, vol)
- def move(self, n, side):
- vol = self.pa.volume
- if self.locked:
- for i, _ in enumerate(vol.values):
- vol.values[i] += n
- else:
- vol.values[side] += n
- PULSE.set_volume(self.pa, vol)
- class Screen():
- DOWN = 1
- UP = -1
- SCROLL_UP = [getattr(curses, i, 0) for i in ['BUTTON4_PRESSED', 'BUTTON3_TRIPLE_CLICKED']]
- SCROLL_DOWN = [getattr(curses, i, 0) for i in ['BUTTON5_PRESSED', 'A_LOW', 'A_BOLD', 'BUTTON4_DOUBLE_CLICKED']]
- KEY_MOUSE = getattr(curses, 'KEY_MOUSE', 0)
- DIGITS = list(map(ord, map(str, range(10))))
- SIDES = {Bar.LEFT: 'Left', Bar.RIGHT: 'Right', Bar.RLEFT: 'Rear Left',
- Bar.RRIGHT: 'Rear Right', Bar.CENTER: 'Center', Bar.SUB: 'Subwoofer',
- Bar.SLEFT: 'Side left', Bar.SRIGHT: 'Side right'}
- SEQ_TO_KEY = {159: curses.KEY_F1, 160: curses.KEY_F2, 161: curses.KEY_F3,
- 316: curses.KEY_SRIGHT, 317: curses.KEY_SLEFT,
- 151: curses.KEY_HOME, 266: curses.KEY_HOME,
- 149: curses.KEY_END, 269: curses.KEY_END}
- def __init__(self, color=2, mouse=True):
- os.environ['ESCDELAY'] = '25'
- self.screen = curses.initscr()
- self.screen.nodelay(True)
- self.screen.scrollok(1)
- if mouse:
- try:
- curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.BUTTON1_CLICKED | self.KEY_MOUSE |
- functools.reduce(operator.or_, list(self.SCROLL_UP)) |
- functools.reduce(operator.or_, list(self.SCROLL_DOWN)))
- except:
- self.KEY_MOUSE = 0
- else:
- self.KEY_MOUSE = 0
- try:
- curses.curs_set(0)
- except: # terminal doesn't support visibility requests
- pass
- self.screen.border(0)
- self.screen.clear()
- self.screen.refresh()
- self.index = 0
- self.top_line_num = 0
- self.focus_line_num = 0
- self.lines, self.cols = curses.LINES - 2, curses.COLS - 1
- self.info, self.menu = str, str
- self.mode_keys = self.get_mode_keys()
- self.menu_titles = ['{} Output'.format(self.mode_keys[0]),
- '{} Input'.format(self.mode_keys[1]),
- '{} Cards'.format(self.mode_keys[2])]
- self.data = []
- self.mode = {0: 1, 1: 0, 2: 0}
- self.modes_data = [[[], 0, 0] for i in range(6)]
- self.active_mode = 0
- self.old_mode = 0
- self.change_mode_allowed = True
- self.n_lines = 0
- self.color_mode = color
- if color in (1, 2) and curses.has_colors():
- curses.start_color()
- curses.use_default_colors()
- curses.init_pair(1, curses.COLOR_GREEN, -1)
- curses.init_pair(2, curses.COLOR_YELLOW, -1)
- curses.init_pair(3, curses.COLOR_RED, -1)
- self.green = curses.color_pair(1)
- self.yellow = curses.color_pair(2)
- self.red = curses.color_pair(3)
- n = 7 if curses.COLORS < 256 else 67
- curses.init_pair(n, n - 1, -1)
- self.muted_color = curses.color_pair(n)
- if curses.COLORS < 256:
- self.gray_gradient = [curses.A_NORMAL] * 3
- else:
- try:
- curses.init_pair(240, 240, -1)
- curses.init_pair(243, 243, -1)
- curses.init_pair(246, 246, -1)
- self.gray_gradient = [curses.color_pair(240),
- curses.color_pair(243),
- curses.color_pair(246)]
- except:
- self.gray_gradient = [curses.A_NORMAL] * 3
- else:
- # if term has colors start them regardless of --color to avoid weird backgrounds on some terminals
- if curses.has_colors():
- curses.start_color()
- curses.use_default_colors()
- self.gray_gradient = [curses.A_NORMAL] * 3
- self.green = self.yellow = self.red = self.muted_color = curses.A_NORMAL
- self.gradient = [self.green, self.yellow, self.red]
- self.submenu_data = []
- self.submenu_width = 30
- self.submenu_show = False
- self.submenu = curses.newwin(curses.LINES, 0, 0, 0)
- self.helpwin_show = False
- self.helpwin = curses.newwin(14, 50, 0, 0)
- try:
- self.helpwin.mvwin((curses.LINES // 2) - 7, (curses.COLS // 2) - 25)
- except:
- pass
- self.selected = None
- self.action = None
- self.server_info = None
- self.ev = threading.Event()
- def getch(self):
- # blocking getch, can be 'interrupted' by ev.set
- self.ev.wait()
- self.ev.clear()
- c = self.screen.getch()
- if c == 27: # collect escape sequences as a single key
- seq_sum = sum(takewhile(lambda x: x != -1, [self.screen.getch() for _ in range(5)]))
- c = self.SEQ_TO_KEY.get(seq_sum, seq_sum + 128 if seq_sum else 27)
- return c
- def pregetcher(self):
- # because curses.getch doesn't work well with threads
- while True:
- select([sys.stdin], [], [], 10)
- self.ev.set()
- def wake_cb(self, *_):
- self.ev.set()
- def display_line(self, index, line, mod=curses.A_NORMAL, win=None):
- shift, win = 0, win or self.screen
- for i in line.split('\n'):
- parts = i.rsplit('|')
- head = ''.join(parts[:-1])
- tail = int(parts[-1] or 0)
- try:
- win.addstr(index, shift, head, tail | mod)
- except:
- win.addstr(min(curses.LINES - 1, index), min(curses.COLS - 1, shift), head, tail | mod)
- shift += len(head)
- def change_mode(self, mode):
- if not self.change_mode_allowed:
- return
- self.modes_data[self.active_mode][1] = self.focus_line_num
- self.modes_data[self.active_mode][2] = self.top_line_num
- self.old_mode = self.active_mode
- self.mode = self.mode.fromkeys(self.mode, 0)
- self.mode[mode] = 1
- self.focus_line_num = self.modes_data[mode][1]
- self.top_line_num = self.modes_data[mode][2]
- self.active_mode = mode
- self.get_data()
- def cycle_mode(self, direction=1):
- for mode, active in self.mode.items():
- if active == 1:
- self.change_mode((mode + direction) % 3)
- return
- def update_menu(self):
- if self.change_mode_allowed:
- self.menu = '{}|{}\n {}|{}\n {}|{}\n {:>{}}|{}'.format(
- self.menu_titles[0], curses.A_BOLD if self.mode[0] else curses.A_DIM,
- self.menu_titles[1], curses.A_BOLD if self.mode[1] else curses.A_DIM,
- self.menu_titles[2], curses.A_BOLD if self.mode[2] else curses.A_DIM,
- "? - help", self.cols - 30, curses.A_DIM)
- else:
- selected = 'output' if type(self.selected[0].pa) is PulseSinkInputInfo else 'input'
- self.menu = "Select new {} device:|{}".format(selected, curses.A_NORMAL)
- def update_info(self):
- focus, bottom = self.focus_line_num + self.top_line_num + 1, self.top_line_num + self.lines
- try:
- bar, side = self.data[focus - 1][0], self.data[focus - 1][1]
- except IndexError:
- self.focus_line_num, self.top_line_num = 0, 0
- for _ in range(len(self.data)): self.scroll(self.UP)
- return
- if side is Bar.NONE:
- self.info = str
- return
- side = 'All' if bar.locked else 'Mono' if bar.channels == 1 else self.SIDES[side]
- more = '↕' if bottom < self.n_lines and self.top_line_num > 0 else '↑' if self.top_line_num > 0 else '↓' if bottom < self.n_lines else ' '
- name = '{}: {}'.format(bar.name, side)
- if len(name) > self.cols - 8:
- name = '{}: {}'.format(bar.name[:self.cols - (10 + len(side))].strip(), side)
- locked = '{}|{}'.format(CFG.style.info_locked, self.red) if bar.locked else '{}|{}'.format(CFG.style.info_unlocked, curses.A_DIM)
- muted = '{}|{}'.format(CFG.style.info_muted, self.red) if bar.muted else '{}|{}'.format(CFG.style.info_unmuted, curses.A_DIM)
- self.info = '{}\n {}\n {}|{}\n{:>{}}|0'.format(locked, muted, name, curses.A_NORMAL, more, self.cols - len(name) - 5)
- def run_mouse(self):
- try:
- _, x, y, _, c = curses.getmouse()
- if c & curses.BUTTON1_CLICKED:
- if y > 0:
- top, bottom = self.top_line_num, len(self.data[self.top_line_num:self.top_line_num + self.lines]) - 1
- if y - 1 <= bottom:
- self.focus_line_num = max(top, min(bottom, y - 1))
- else:
- f1 = len(self.menu_titles[0]) + 1 # 1 is 'spacing' after the title
- f2 = f1 + len(self.menu_titles[1]) + 2
- f3 = f2 + len(self.menu_titles[2]) + 3
- if x in range(0, f1):
- self.change_mode(0)
- elif x in range(f1, f2):
- self.change_mode(1)
- elif x in range(f2, f3):
- self.change_mode(2)
- return c
- except curses.error:
- return None
- def resize(self):
- curses.COLS, curses.LINES = get_terminal_size()
- curses.resizeterm(curses.LINES, curses.COLS)
- self.submenu.resize(curses.LINES, self.submenu_width + 1)
- if self.submenu_show:
- self.submenu_show = False
- self.focus_line_num = self.modes_data[5][1]
- self.top_line_num = self.modes_data[5][2]
- try:
- self.helpwin.resize(14, 50)
- self.helpwin.mvwin((curses.LINES // 2) - 7, (curses.COLS // 2) - 25)
- except curses.error:
- pass
- self.helpwin_show = False
- self.lines, self.cols = curses.LINES - 2, curses.COLS - 1
- self.ev.set()
- def terminate(self):
- # if ^C pressed while sleeping in reconnect wrapper.restore won't be called
- # so have to restore it manually here
- self.screen.keypad(0)
- curses.echo()
- curses.nocbreak()
- curses.endwin()
- sys.exit()
- def reconnect(self):
- self.focus_line_num = 0
- self.menu = self.info = str
- self.data = [(Bar('PA - Connection refused.\nTrying to reconnect.'), Bar.NONE, 0)]
- while not PULSE.connected:
- self.display()
- if self.screen.getch() in CFG.keys.quit: sys.exit()
- PULSE.reconnect()
- sleep(0.5)
- PULSE.subscribe(self.wake_cb)
- self.ev.set()
- def run(self, _):
- signal.signal(signal.SIGINT, lambda s, f: self.terminate())
- signal.signal(signal.SIGTERM, lambda s, f: self.terminate())
- signal.signal(signal.SIGWINCH, lambda s, f: self.resize())
- threading.Thread(target=self.pregetcher, daemon=True).start()
- PULSE.subscribe(self.wake_cb)
- self.ev.set()
- while True:
- try:
- if not self.submenu_show:
- try:
- self.get_data()
- except RuntimeError:
- self.reconnect()
- except IndexError:
- self.scroll(self.UP)
- if self.helpwin_show:
- self.display_helpwin()
- self.run_helpwin()
- continue
- self.update_menu()
- self.update_info()
- self.display()
- elif self.change_mode_allowed:
- self.display_submenu()
- self.run_submenu()
- continue
- except (curses.error, IndexError, ValueError) as e:
- self.screen.erase()
- self.screen.addstr("Terminal *might* be too small {}:{}\n".format(curses.LINES, curses.COLS))
- self.screen.addstr("{}\n{}\n".format(str(self.mode), str(e)))
- self.screen.addstr(str(traceback.extract_tb(e.__traceback__)))
- c = self.getch()
- if c == -1: continue
- focus = self.top_line_num + self.focus_line_num
- bar, side = self.data[focus][0], self.data[focus][1]
- if c == self.KEY_MOUSE:
- c = self.run_mouse() or c
- if c in CFG.keys.mode1:
- self.change_mode(0)
- elif c in CFG.keys.mode2:
- self.change_mode(1)
- elif c in CFG.keys.mode3:
- self.change_mode(2)
- elif c == ord('?'):
- self.helpwin_show = True
- elif c == ord('\n'):
- if not self.submenu_show and self.change_mode_allowed and side != Bar.NONE:
- self.selected = self.data[focus]
- if type(self.selected[0].pa) in (PulseSinkInfo, PulseSourceInfo):
- self.submenu_data = ['Suspend', 'Resume', 'Set as default']
- if self.selected[0].pa.n_ports:
- self.submenu_data.append('Set port')
- elif type(self.selected[0].pa) is PulseCard:
- self.fill_submenu_pa(target='profile', off=0, hide=CFG.ui.hide_unavailable_profiles)
- else:
- self.submenu_data = ['Move', 'Kill']
- self.submenu_show = True
- self.modes_data[5][0] = 0
- self.modes_data[5][1] = self.focus_line_num
- self.modes_data[5][2] = self.top_line_num
- self.focus_line_num = self.top_line_num = 0
- self.n_lines = len(self.submenu_data)
- self.resize_submenu()
- elif not self.change_mode_allowed:
- self.submenu_show = False
- self.change_mode_allowed = True
- if self.action == 'Move':
- if type(self.selected[0].pa) is PulseSinkInputInfo:
- PULSE.sink_input_move(self.selected[0].index, self.data[focus][0].pa.index)
- elif type(self.selected[0].pa) is PulseSourceOutputInfo:
- PULSE.source_output_move(self.selected[0].index, self.data[focus][0].pa.index)
- self.change_mode(self.old_mode)
- self.focus_line_num = self.modes_data[5][1]
- self.top_line_num = self.modes_data[5][2]
- else:
- self.change_mode(self.old_mode)
- elif c in CFG.keys.next_mode:
- self.cycle_mode()
- elif c in CFG.keys.prev_mode:
- self.cycle_mode(direction=-1)
- elif c in CFG.keys.quit:
- if not self.change_mode_allowed:
- self.submenu_show = False
- self.change_mode_allowed = True
- self.change_mode(self.old_mode)
- self.focus_line_num = self.modes_data[5][1]
- self.top_line_num = self.modes_data[5][2]
- else:
- sys.exit()
- if side is Bar.NONE:
- continue
- if c in CFG.keys.up:
- if bar.locked:
- if self.data[focus][1] == 0:
- n = 1
- else:
- n = self.data[focus][1] + 1
- for _ in range(n): self.scroll(self.UP)
- else:
- self.scroll(self.UP)
- if not self.data[self.top_line_num + self.focus_line_num][0]:
- self.scroll(self.UP)
- elif c in CFG.keys.down:
- if bar.locked:
- if self.data[focus][1] == self.data[focus][3] - 1:
- n = 1
- else:
- n = ((self.data[focus][3] - 1) - self.data[focus][1]) + 1
- for _ in range(n): self.scroll(self.DOWN)
- else:
- self.scroll(self.DOWN)
- if not self.data[self.top_line_num + self.focus_line_num][0]:
- self.scroll(self.DOWN)
- elif c in CFG.keys.top:
- self.scroll_first()
- elif c in CFG.keys.bottom:
- self.scroll_last()
- elif c in CFG.keys.mute:
- bar.mute_toggle()
- elif c in CFG.keys.lock:
- bar.lock_toggle()
- elif c in CFG.keys.left or any([c & i for i in self.SCROLL_DOWN]):
- bar.move(-CFG.general.step, side)
- elif c in CFG.keys.right or any([c & i for i in self.SCROLL_UP]):
- bar.move(CFG.general.step, side)
- elif c in CFG.keys.left_big:
- bar.move(-CFG.general.step_big, side)
- elif c in CFG.keys.right_big:
- bar.move(CFG.general.step_big, side)
- elif c in self.DIGITS:
- percent = int(chr(c)) * 10
- bar.set(100 if percent == 0 else percent, side)
- def fill_submenu_pa(self, target, off, hide):
- self.submenu_data = []
- active = getattr(self.selected[0].pa, "active_" + target).description.decode()
- for i in getattr(self.selected[0].pa, target + "s"):
- description = i.description.decode()
- if active == description:
- self.submenu_data.append(' {}|{}'.format(description, self.green))
- else:
- if hide and i.available == off: continue
- self.submenu_data.append(' {}|{}'.format(description, curses.A_DIM if i.available == off else 0))
- def build(self, target, devices, streams):
- tmp = []
- index = 0
- for device in devices:
- index += device.volume.channels
- stream_index = device.volume.channels
- tmp.append([device, device.volume.channels, index, stream_index])
- device_index = len(tmp) - 1
- for stream in streams:
- if stream.owner == device.index:
- index += stream.volume.channels
- stream_index += stream.volume.channels
- tmp.append([stream, -1, index, stream_index])
- tmp[device_index][1] += stream.volume.channels
- tmp[-1][1] = tmp[device_index][1]
- for s in tmp:
- found = False
- for i, data in enumerate(target):
- if s[0].index == data[2] and type(s[0]) == type(data[0].pa):
- found = True
- data[0].poll_data(s[0], s[1], s[3])
- y = s[2] - (data[3] - data[1])
- target[i], target[y] = target[y], target[i]
- if not found:
- bar = Bar(s[0])
- bar.owned = s[1]
- bar.stream_index = s[3]
- for c in range(s[0].volume.channels):
- target.append((bar, c, s[0].index, s[0].volume.channels))
- for i in reversed(range(len(target))):
- data = target[i]
- for s in tmp:
- if s[0].index == data[2] and type(s[0]) == type(data[0].pa):
- y = s[2] - (data[3] - data[1])
- target[i], target[y] = target[y], target[i]
- break
- else:
- del target[i]
- if self.focus_line_num + self.top_line_num >= i:
- self.scroll(self.UP)
- return target
- def add_spacers(self, f):
- tmp = []
- l = len(f)
- for i, s in enumerate(f):
- tmp.append(s)
- if s[0].stream_index == s[0].owned and s[1] == s[0].channels - 1 and i != l - 1:
- tmp.append((None, -1, 0, 0))
- return tmp
- def get_data(self):
- if self.mode[0]:
- self.data = self.build(self.modes_data[0][0], PULSE.sink_list(), PULSE.sink_input_list())
- self.data = self.add_spacers(self.data)
- elif self.mode[1]:
- ids = (b'org.PulseAudio.pavucontrol', b'org.gnome.VolumeControl', b'org.kde.kmixd', b'pulsemixer')
- source_output_list = [s for s in PULSE.source_output_list() if s.application_id not in ids]
- self.data = self.build(self.modes_data[1][0], PULSE.source_list(), source_output_list)
- self.data = self.add_spacers(self.data)
- elif self.mode[2]:
- self.data = self.build(self.modes_data[2][0], PULSE.card_list(), [])
- elif type(self.selected[0].pa) is PulseSinkInputInfo:
- self.data = self.build(self.modes_data[3][0], PULSE.sink_list(), [])
- elif type(self.selected[0].pa) is PulseSourceOutputInfo:
- self.data = self.build(self.modes_data[4][0], PULSE.source_list(), [])
- self.server_info = PULSE.get_server_info()
- self.n_lines = len(self.data)
- if not self.n_lines:
- self.focus_line_num = 0
- self.data.append((Bar('no data'), Bar.NONE, 0))
- if not self.data[self.top_line_num + self.focus_line_num][0]:
- self.scroll(self.UP)
- def display(self):
- self.screen.erase()
- top = self.top_line_num
- bottom = self.top_line_num + self.lines
- self.display_line(0, self.menu)
- for index, line in enumerate(self.data[top:bottom]):
- bar, bartype = line[0], line[1]
- if not bar:
- self.screen.addstr(index + 1, 0, '', curses.A_DIM)
- continue
- elif bartype is Bar.NONE:
- for i, name in enumerate(bar.name.split('\n')):
- self.screen.addstr((self.lines // 2) + i, (self.cols // 2) - len(name) // 2, name, curses.A_DIM)
- break
- # hightlight lines from same bar
- same = []
- for i, v in enumerate(self.data[top:bottom]):
- if v[0] is self.data[self.top_line_num + self.focus_line_num][0]:
- same.append(v[0])
- tree = ' '
- if bar.owner == -1 and bar.owned > bar.channels:
- tree = ' │'
- if bar.owner != -1:
- tree = ' │'
- if bartype == Bar.LEFT:
- if bar.owner == -1:
- tree = ' '
- if bar.owner != -1:
- tree = ' ├─'
- if bar.stream_index == bar.owned:
- tree = ' └─'
- if bar.channels != 1:
- brackets = [CFG.style.bar_top_left, CFG.style.bar_top_right]
- else:
- brackets = [CFG.style.bar_left_mono, CFG.style.bar_right_mono]
- elif bartype == bar.channels - 1:
- if bar.stream_index == bar.owned:
- tree = ' '
- brackets = [CFG.style.bar_bottom_left, CFG.style.bar_bottom_right]
- else:
- if bar.stream_index == bar.owned:
- tree = ' '
- brackets = ['├', '┤']
- # focus current lines
- focus_hl, bracket_hl, arrow, gradient = 0, 0, CFG.style.arrow, self.gradient
- if index == self.focus_line_num:
- focus_hl = bracket_hl = curses.A_BOLD
- arrow = CFG.style.arrow_focused
- elif bar in same:
- focus_hl = curses.A_BOLD
- if bar.locked:
- bracket_hl = curses.A_BOLD
- arrow = CFG.style.arrow_locked
- elif not bar.muted and self.color_mode != 2:
- gradient = self.gray_gradient
- # highlight chosen sink/source or muted
- if not self.change_mode_allowed and self.selected[0].owner == self.data[index][0].index:
- bracket_hl = self.green | bracket_hl
- if bar.muted:
- focus_hl = focus_hl | self.muted_color
- elif bar.muted:
- bracket_hl = bracket_hl | self.red
- focus_hl = focus_hl | self.muted_color
- off = 6 * (self.cols // (43 if self.cols <= 60 else 25)) - len(tree)
- cols = self.cols - 31 - off - len(tree)
- vol = list(CFG.style.bar_off * (cols - (cols % 3 != 0)))
- n = int(len(vol) * bar.volume[bartype] / bar.maxsize)
- if bar.muted:
- vol[:n] = CFG.style.bar_on_muted * n
- else:
- vol[:n] = CFG.style.bar_on * n
- vol = ''.join(vol)
- if bartype is Bar.LEFT:
- if bar.pa.name in (self.server_info.default_sink_name, self.server_info.default_source_name):
- tree = CFG.style.default_stream
- name = '{}{}'.format(bar.name, bar.media_name)
- if bar.media_name_wide and len(bar.name) + sum(bar.media_name_widths) > 20 + off:
- to_remove, widths = 0, [1] * len(bar.name) + bar.media_name_widths
- while sum(widths) > 20 + off:
- widths.pop()
- to_remove += 1
- name = name[:-to_remove].strip() + '~'
- elif len(name) > 20 + off:
- name = name[:20 + off].strip() + '~'
- line = '{:<{}}|{}\n {:<3}|{}\n '.format(name, 22 + off, focus_hl,
- '' if type(bar.pa) is PulseCard else bar.volume[0],
- focus_hl)
- elif bartype is Bar.RIGHT:
- line = '{:>{}}|{}\n {}|{}\n {:<3}|{}\n '.format(
- '', 21 + off, self.red if bar.locked else curses.A_DIM,
- '', self.red if bar.muted else curses.A_DIM,
- bar.volume[bartype], focus_hl)
- else:
- line = '{:>{}}{:<3}|{}\n '.format('', 23 + off, bar.volume[bartype], focus_hl)
- if type(bar.pa) is PulseCard:
- volbar = '\n{}|0'.format(bar.pa.active_profile.description.decode()[:len(vol)])
- brackets = [' ', ' ']
- else:
- volbar = ''
- for i, v in enumerate(re.findall('.{{{}}}'.format((len(vol) // 3)), vol)):
- volbar += '\n{}|{}'.format(v, gradient[i] | focus_hl)
- line += '{:>1}|{}\n{}|{}{}\n{}|{}\n{}|{}'.format(arrow, curses.A_BOLD,
- brackets[0], bracket_hl,
- volbar,
- brackets[1], bracket_hl,
- arrow, curses.A_BOLD)
- self.display_line(index + 1, tree + "|0\n" + line)
- self.display_line(self.lines + 1, self.info)
- self.screen.refresh()
- def get_mode_keys(self):
- return [re.compile(r'[()]|KEY_').sub('', curses.keyname(k[0]).decode('utf-8')) for k in [
- CFG.keys.mode1, CFG.keys.mode2, CFG.keys.mode3]]
- def display_helpwin(self):
- doc = (('j k ↑ ↓', 'Navigation'),
- ('h l ← →', 'Change volume'),
- ('H L Shift← Shift→', 'Change volume by 10'),
- ('1 2 3 .. 8 9 0', 'Set volume to 10%-100%'),
- ('m', 'Mute/Unmute'),
- ('Space', 'Lock/Unlock channels'),
- ('Enter', 'Context menu'),
- ('{} {} {}'.format(*self.mode_keys), 'Change modes'),
- ('Tab Shift Tab', 'Next/Previous mode'),
- ('Mouse click', 'Select device or mode'),
- ('Mouse wheel', 'Volume change'),
- ('Esc q', 'Quit'))
- win_width, desc_maxlen = self.helpwin.getmaxyx()[1] - 4, max(len(x[1]) for x in doc)
- self.helpwin.erase()
- for i, s in enumerate(doc):
- self.helpwin.addstr(i + 1, 2, s[0] + ' ' * (win_width - desc_maxlen - len(s[0])) + s[1])
- self.helpwin.border()
- self.helpwin.refresh()
- def run_helpwin(self):
- if self.getch() in CFG.keys.quit:
- self.helpwin_show = False
- def resize_submenu(self):
- key = lambda x: len(x.split('|')[0])
- self.submenu_width = min(self.cols + 1, max(30, len(max(self.submenu_data, key=key).split('|')[0]) + 3))
- self.submenu.resize(curses.LINES, self.submenu_width + 1)
- def display_submenu(self):
- top = self.top_line_num
- bottom = self.top_line_num + self.lines + 2
- self.submenu.erase()
- self.submenu.vline(0, self.submenu_width, curses.ACS_VLINE, curses.LINES)
- for index, line in enumerate(self.submenu_data[top:bottom]):
- if index == self.focus_line_num:
- focus_hl = curses.A_BOLD
- arrow = CFG.style.arrow_focused
- else:
- focus_hl = curses.A_NORMAL
- arrow = ' '
- if '|' in line:
- self.display_line(index, ' {}|0\n'.format(arrow) + line, focus_hl, win=self.submenu)
- else:
- self.submenu.addstr(index, 1, arrow + ' ' + line, focus_hl)
- self.submenu.refresh()
- def run_submenu(self):
- c = self.getch()
- if c in CFG.keys.quit:
- self.submenu_show = False
- self.focus_line_num = self.modes_data[5][1]
- self.top_line_num = self.modes_data[5][2]
- elif c in CFG.keys.up:
- self.scroll(self.UP, cycle=True)
- elif c in CFG.keys.down:
- self.scroll(self.DOWN, cycle=True)
- elif c in CFG.keys.top:
- self.scroll_first()
- elif c in CFG.keys.bottom:
- self.scroll_last()
- elif c == ord('\n'):
- focus = self.focus_line_num + self.top_line_num
- self.action = self.submenu_data[focus]
- if self.action == 'Move':
- if self.active_mode == 0:
- self.change_mode(3)
- elif self.active_mode == 1:
- self.change_mode(4)
- self.change_mode_allowed = self.submenu_show = False
- return
- elif self.action == 'Kill':
- try:
- PULSE.kill_client(self.selected[0].pa.client.index)
- except:
- if type(self.selected[0].pa) is PulseSinkInputInfo:
- PULSE.kill_sink(self.selected[2])
- else:
- PULSE.kill_source(self.selected[2])
- elif self.action == 'Suspend':
- if type(self.selected[0].pa) is PulseSinkInfo:
- PULSE.sink_suspend(self.selected[2], 1)
- else:
- PULSE.source_suspend(self.selected[2], 1)
- elif self.action == 'Resume':
- if type(self.selected[0].pa) is PulseSinkInfo:
- PULSE.sink_suspend(self.selected[2], 0)
- else:
- PULSE.source_suspend(self.selected[2], 0)
- elif self.action == 'Set as default':
- if type(self.selected[0].pa) is PulseSinkInfo:
- PULSE.set_default_sink(self.selected[0].pa.name)
- else:
- PULSE.set_default_source(self.selected[0].pa.name)
- elif self.action == 'Set port':
- self.fill_submenu_pa(target='port', off=1, hide=CFG.ui.hide_unavailable_ports)
- self.focus_line_num = self.top_line_num = 0
- self.n_lines = len(self.submenu_data)
- return
- else:
- index = self.selected[0].pa.index
- description = self.action.rsplit('|')[0].strip()
- get_name = lambda desc, l: next(filter(lambda x: x.description.decode() == desc, l)).name
- if type(self.selected[0].pa) is PulseSinkInfo:
- PULSE.set_sink_port(index, get_name(description, self.selected[0].pa.ports))
- elif type(self.selected[0].pa) is PulseSourceInfo:
- PULSE.set_source_port(index, get_name(description, self.selected[0].pa.ports))
- elif type(self.selected[0].pa) is PulseCard:
- PULSE.set_card_profile(index, get_name(description, self.selected[0].pa.profiles))
- self.change_mode_allowed = True
- self.submenu_show = False
- self.focus_line_num = self.modes_data[5][1]
- self.top_line_num = self.modes_data[5][2]
- def scroll(self, n, cycle=False):
- next_line_num = self.focus_line_num + n
- if n == self.UP and self.focus_line_num == 0 and self.top_line_num != 0:
- self.top_line_num += self.UP
- return
- elif n == self.DOWN and next_line_num == self.lines and (self.top_line_num + self.lines) != self.n_lines:
- self.top_line_num += self.DOWN
- return
- if n == self.UP:
- if self.top_line_num != 0 or self.focus_line_num != 0:
- self.focus_line_num = next_line_num
- elif cycle:
- self.scroll_last()
- elif n == self.DOWN and self.focus_line_num != self.lines:
- if self.top_line_num + self.focus_line_num + 1 != self.n_lines:
- self.focus_line_num = next_line_num
- elif cycle:
- self.scroll_first()
- def scroll_first(self):
- for _ in range(self.n_lines): self.scroll(self.UP)
- def scroll_last(self):
- for _ in range(self.n_lines): self.scroll(self.DOWN)
- class Config():
- def __init__(self):
- class General:
- step = 1
- step_big = 10
- server = None
- self._more_keys = {'KEY_ESC': 27, 'KEY_TAB': 9, 'C': -96, 'M': 128}
- class Keys:
- up = [ord('k'), curses.KEY_UP, curses.KEY_PPAGE]
- down = [ord('j'), curses.KEY_DOWN, curses.KEY_NPAGE]
- left = [ord('h'), curses.KEY_LEFT]
- right = [ord('l'), curses.KEY_RIGHT]
- left_big = [ord('H'), curses.KEY_SLEFT]
- right_big = [ord('L'), curses.KEY_SRIGHT]
- top = [ord('g'), curses.KEY_HOME]
- bottom = [ord('G'), curses.KEY_END]
- mode1 = [curses.KEY_F1]
- mode2 = [curses.KEY_F2]
- mode3 = [curses.KEY_F3]
- next_mode = [self._more_keys['KEY_TAB']]
- prev_mode = [curses.KEY_BTAB]
- mute = [ord('m')]
- lock = [ord(' ')]
- quit = [ord('q'), self._more_keys['KEY_ESC']]
- class UI:
- hide_unavailable_profiles = False
- hide_unavailable_ports = False
- color = 2
- mouse = True
- class Style:
- _bar_style = os.getenv('PULSEMIXER_BAR_STYLE', '┌╶┐╴└┘▮▯- ──').ljust(12, '?')
- bar_top_left = _bar_style[0]
- bar_left_mono = _bar_style[1]
- bar_top_right = _bar_style[2]
- bar_right_mono = _bar_style[3]
- bar_bottom_left = _bar_style[4]
- bar_bottom_right = _bar_style[5]
- bar_on = _bar_style[6]
- bar_on_muted = _bar_style[7]
- bar_off = _bar_style[8]
- arrow = _bar_style[9]
- arrow_focused = _bar_style[10]
- arrow_locked = _bar_style[11]
- default_stream = '*'
- info_locked = 'L'
- info_unlocked = 'U'
- info_muted = 'M'
- info_unmuted = 'M'
- self.general = General()
- self.keys = Keys()
- self.ui = UI()
- self.style = Style()
- self.renames = {}
- self.path = os.getenv("XDG_CONFIG_HOME", os.path.join(os.path.expanduser("~"), ".config")) + '/pulsemixer.cfg'
- def save(self):
- default = '''
- ;; Goes into ~/.config/pulsemixer.cfg, $XDG_CONFIG_HOME respected
- ;; Everything that starts with "#" or ";" is a comment
- ;; For the option to take effect simply uncomment it
- [general]
- step = 1
- step-big = 10
- ; server =
- [keys]
- ;; To bind "special keys" such as arrows see "Key constant" table in
- ;; https://docs.python.org/3/library/curses.html#constants
- ; up = k, KEY_UP, KEY_PPAGE
- ; down = j, KEY_DOWN, KEY_NPAGE
- ; left = h, KEY_LEFT
- ; right = l, KEY_RIGHT
- ; left-big = H, KEY_SLEFT
- ; right-big = L, KEY_SRIGHT
- ; top = g, KEY_HOME
- ; bottom = G, KEY_END
- ; mode1 = KEY_F1
- ; mode2 = KEY_F2
- ; mode3 = KEY_F3
- ; next-mode = KEY_TAB
- ; prev-mode = KEY_BTAB
- ; mute = m
- ; lock = ' ' ; 'space', quotes are stripped
- ; quit = q, KEY_ESC
- [ui]
- ; hide-unavailable-profiles = no
- ; hide-unavailable-ports = no
- ; color = 2 ; same as --color, 0 no color, 1 color currently selected, 2 full-color
- ; mouse = yes
- [style]
- ;; Pulsemixer will use these characters to draw interface
- ;; Single characters only
- ; bar-top-left = ┌
- ; bar-left-mono = ╶
- ; bar-top-right = ┐
- ; bar-right-mono = ╴
- ; bar-bottom-left = └
- ; bar-bottom-right = ┘
- ; bar-on = ▮
- ; bar-on-muted = ▯
- ; bar-off = -
- ; arrow = ' '
- ; arrow-focused = ─
- ; arrow-locked = ─
- ; default-stream = *
- ; info-locked = L
- ; info-unlocked = U
- ; info-muted = M ; 🔇
- ; info-unmuted = M ; 🔉
- [renames]
- ;; Changes stream names in interactive mode, regular expression are supported
- ;; https://docs.python.org/3/library/re.html#regular-expression-syntax
- ; 'default name example' = 'new name'
- ; '(?i)built-in .* audio' = 'Audio Controller'
- ; 'AudioIPC Server' = 'Firefox'
- '''
- directory = self.path.rsplit('/', 1)[0]
- if not os.path.exists(directory):
- os.makedirs(directory)
- with open(self.path, 'w') as configfile:
- configfile.write(dedent(default).strip())
- return self.path
- def load(self):
- parser = ConfigParser(inline_comment_prefixes=('#', ';'), empty_lines_in_values=False)
- parser.optionxform = str # keep case of keys, lowered() later
- parser.NONSPACECRE = re.compile(r"") # ignore leading whitespace
- if not parser.read(self.path): return self
- if parser.has_section('renames'):
- self.renames = {re.compile(k.strip('"\'') + r'\Z'):v.strip('"\'') for k, v in parser.items('renames')}
- parser.remove_section('renames')
- def getkeys(s, k):
- keys = []
- for i in parser.get(s, k).strip(',').split(','):
- i = i.strip().strip('"\'') # in case 'key' is encountered
- if len(i) > 1:
- if i.startswith(('C-', 'M-')):
- mod, key = i.split('-')
- key = self._more_keys[mod] + ord(key.lower())
- else:
- key = getattr(curses, i, self._more_keys.get(i))
- else:
- key = ord(i)
- if key is None: raise Exception("module 'curses' has no attribute {}".format(i))
- keys.append(key)
- return keys
- get = {str: lambda s, k: parser.get(s, k).strip('"\''),
- None.__class__: lambda s, k: parser.get(s, k).encode(), # server
- list: getkeys, bool: parser.getboolean,
- int: parser.getint, float: parser.getfloat}
- for section in parser.sections():
- for key in parser[section]:
- pykey = key.lower().replace('-', '_')
- pyval = getattr(getattr(self, section.lower()), pykey)
- val = get[type(pyval)](section, key)
- setattr(getattr(self, section.lower()), pykey, val)
- return self
- PULSE = CFG = None
- def main():
- try:
- opts, args = getopt.getopt(
- sys.argv[1:], "hvl",
- ["help", "version", "list", "list-sinks", "list-sources", "id=",
- "set-volume=", "set-volume-all=", "change-volume=", "max-volume=",
- "get-mute", "toggle-mute", "mute", "unmute", "get-volume",
- "color=", "server=", "no-mouse", "create-config"])
- except getopt.GetoptError as e:
- sys.exit("ERR: {}".format(e))
- assert args == [], sys.exit('ERR: {} not not recognized'.format(' '.join(args).strip()))
- dopts = dict(opts)
- if '-h' in dopts or '--help' in dopts:
- sys.exit(print(__doc__))
- if '-v' in dopts or '--version' in dopts:
- sys.exit(print(VERSION))
- if '--create-config' in dopts:
- try:
- sys.exit(print(Config().save()))
- except Exception as e: # permission denied and such
- sys.exit('ERR: {}'.format(e))
- global PULSE, CFG
- try:
- CFG = Config().load()
- except Exception as e:
- sys.exit('ERR: {}'.format(e))
- CFG.general.server = dopts.get('--server', '').encode() or CFG.general.server
- CFG.ui.mouse = False if '--no-mouse' in dopts else CFG.ui.mouse
- try:
- CFG.ui.color = min(2, max(0, int(dopts.get('--color', '') or CFG.ui.color)))
- except:
- sys.exit('ERR: color must be a number')
- signal.signal(signal.SIGINT, lambda s, f: sys.exit(1))
- PULSE = Pulse('pulsemixer', CFG.general.server)
- noninteractive_opts = dict(dopts)
- noninteractive_opts.pop('--server', None)
- noninteractive_opts.pop('--color', None)
- noninteractive_opts.pop('--no-mouse', None)
- if not noninteractive_opts:
- if not sys.stdout.isatty(): sys.exit('ERR: output is not a tty-like device')
- title = 'pulsemixer {}'.format(CFG.general.server.decode() if CFG.general.server else '')
- print('\033]2;{}\007'.format(title.strip()), end='', flush=True)
- curses.wrapper(Screen(CFG.ui.color, CFG.ui.mouse).run)
- sinks = PULSE.sink_list()
- sink_inputs = PULSE.sink_input_list()
- sources = PULSE.source_list()
- source_outputs = PULSE.source_output_list()
- server_info = PULSE.get_server_info()
- streams = OrderedDict()
- for k, v in (('sink-', sinks), ('sink-input-', sink_inputs), ('source-', sources), ('source-output-', source_outputs)):
- for stream in v: streams[k + str(stream.index)] = stream
- check_n = lambda x, err: x.strip('+-').isdigit() or sys.exit('ERR: {} must be a number'.format(err))
- check_id = lambda x: x in streams or sys.exit('ERR: No such ID: ' + str(x))
- from_old_id = lambda index: next((k for k in streams if k.rsplit('-', 1)[-1] == index), index)
- print_default = lambda x, y: print(x == y and ', Default' or '')
- if '--id' in dopts:
- index = [i for i in opts if '--id' in i][0][1]
- if index.isdigit(): index = from_old_id(index)
- else:
- index = 'sink-{}'.format([s.index for s in sinks if s.name == server_info.default_sink_name][0])
- check_id(index)
- max_volume = 150
- for opt, arg in opts:
- if opt == '--id':
- index = arg
- if index.isdigit(): index = from_old_id(index)
- check_id(index)
- max_volume = 150 # reset for each new id
- elif opt in ('-l', '--list'):
- for sink in sinks:
- print("Sink:\t\t", sink, end='')
- print_default(sink.name, server_info.default_sink_name)
- for sink in sink_inputs:
- print("Sink input:\t", sink)
- for source in sources:
- print("Source:\t\t", source, end='')
- print_default(source.name, server_info.default_source_name)
- for source in source_outputs:
- print("Source output:\t", source)
- elif opt == '--list-sinks':
- for sink in sinks:
- print("Sink:\t\t", sink, end='')
- print_default(sink.name, server_info.default_sink_name)
- for sink in sink_inputs:
- print("Sink input:\t", sink)
- elif opt == '--list-sources':
- for source in sources:
- print("Source:\t\t", source, end='')
- print_default(source.name, server_info.default_source_name)
- for source in source_outputs:
- print("Source output:\t", source)
- elif opt == '--get-mute':
- print(streams[index].mute)
- elif opt == '--mute':
- PULSE.mute_stream(streams[index])
- elif opt == '--unmute':
- PULSE.unmute_stream(streams[index])
- elif opt == '--toggle-mute':
- PULSE.unmute_stream(streams[index]) if streams[index].mute else PULSE.mute_stream(streams[index])
- elif opt == '--get-volume':
- print(*streams[index].volume.values)
- elif opt == '--set-volume':
- check_n(arg, err='volume')
- vol = streams[index].volume
- for i, _ in enumerate(vol.values):
- vol.values[i] = int(arg)
- PULSE.set_volume(streams[index], vol)
- elif opt == '--set-volume-all':
- vol = streams[index].volume
- arg = arg.strip(':').split(':')
- if len(arg) != len(vol.values):
- sys.exit("ERR: Specified volumes not equal to the number of channels in the stream")
- for i, _ in enumerate(vol.values):
- check_n(arg[i], err='volume')
- vol.values[i] = int(arg[i])
- PULSE.set_volume(streams[index], vol)
- elif opt == '--change-volume':
- check_n(arg, err='volume')
- vol = streams[index].volume
- for i, _ in enumerate(vol.values):
- vol.values[i] = min(vol.values[i] + int(arg), max_volume)
- PULSE.set_volume(streams[index], vol)
- elif opt == '--max-volume':
- check_n(arg, err='max volume')
- max_volume = int(arg)
- vol = streams[index].volume
- for i, _ in enumerate(vol.values):
- vol.values[i] = min(vol.values[i], max_volume)
- PULSE.set_volume(streams[index], vol)
- if __name__ == '__main__':
- main()
|