#!/usr/bin/env python3
'''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. If no ID specified - default sink is used
  --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
  --get-mute            get mute for ID
  --toggle-mute         toggle mute for ID
  --get-volume          get volume for ID
  --mute                mute ID
  --unmute              unmute ID
  --server              choose the server to connect to
  --color n             0 no color, 1 color currently selected, 2 full-color (default)
  --no-mouse            disable mouse support
  --create-config       generate configuration file'''

VERSION = '1.4.0'

import curses
import functools
import getopt
import operator
import os
import re
import signal
import sys
import threading
import traceback
from configparser import ConfigParser
from ctypes import *
from pprint import pprint
from select import select
from textwrap import dedent
from time import sleep

#########################################################################################
# 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 PA_THREADED_MAINLOOP(Structure):
    pass


class PA_MAINLOOP_API(Structure):
    pass


class PA_CONTEXT(Structure):
    pass


class PA_OPERATION(Structure):
    pass


class PA_PROPLIST(Structure):
    pass


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_SIGNAL_CB_T             = CFUNCTYPE(c_void_p, POINTER(PA_MAINLOOP_API), POINTER(c_int), c_int, 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_signal_init = DLL.pa_signal_init
pa_signal_init.restype = c_int
pa_signal_init.argtypes = [POINTER(PA_MAINLOOP_API)]

pa_signal_new = DLL.pa_signal_new
pa_signal_new.restype = None
pa_signal_new.argtypes = [c_int, PA_SIGNAL_CB_T, POINTER(c_void_p)]

pa_context_errno = DLL.pa_context_errno
pa_context_errno.restype = c_int
pa_context_errno.argtypes = [POINTER(PA_CONTEXT)]

pa_context_new = DLL.pa_context_new
pa_context_new.restype = POINTER(PA_CONTEXT)
pa_context_new.argtypes = [POINTER(PA_MAINLOOP_API), c_char_p]

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_gets = DLL.pa_proplist_gets
pa_proplist_gets.restype = c_char_p
pa_proplist_gets.argtypes = [POINTER(PA_PROPLIST), c_char_p]

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_signal_cb = PA_SIGNAL_CB_T(self.signal_cb)
        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_info':    PA_SINK_INPUT_INFO_CB_T(self.sink_input_cb),
                       'source_output_info': PA_SOURCE_OUTPUT_INFO_CB_T(self.source_output_cb),
                       'sink_info':          PA_SINK_INFO_CB_T(self.sink_cb),
                       'source_info':        PA_SOURCE_INFO_CB_T(self.source_cb),
                       'server_info':        PA_SERVER_INFO_CB_T(self.server_cb),
                       'card_info':          PA_CARD_INFO_CB_T(self.card_cb),
                       'client_info':        PA_CLIENT_INFO_CB_T(self.client_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)

        self.context = pa_context_new(self.mainloop_api, self.client_name)
        pa_context_set_state_callback(self.context, self.pa_state_cb, None)

        if not reconnect: assert pa_signal_init(self.mainloop_api) == 0, "pa_signal_init failed"
        pa_signal_new(2, self.pa_signal_cb, None)
        pa_signal_new(15, self.pa_signal_cb, None)
        if pa_context_connect(self.context, self.server_name, 0, None) < 0:
            self.disconnect()
            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)
        pa_threaded_mainloop_wait(self.mainloop) or pa_threaded_mainloop_unlock(self.mainloop)
        if 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):
        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 signal_cb(self, api, e, sig, userdata):
        if sig in (2, 15):
            self.disconnect()

    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)))
            if not self.connected:   # failed to __init__, can happen with invalid server_name
                pa_threaded_mainloop_signal(self.mainloop, 0)
            self.connected = False
            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)
            func(self, *args)
            self.wait_and_unlock()
            data, self.data = self.data, []
            return data or []
        return wrapper

    @_eof_cb
    def card_cb(self, c, card_info, eof, userdata):
        self.data.append(PulseCard(card_info[0]))

    @_eof_cb
    def client_cb(self, c, client_info, eof, userdata):
        self.data.append(PulseClient(client_info[0]))

    @_eof_cb
    def sink_input_cb(self, c, sink_input_info, eof, userdata):
        self.data.append(PulseSinkInputInfo(sink_input_info[0]))

    @_eof_cb
    def sink_cb(self, c, sink_info, eof, userdata):
        self.data.append(PulseSinkInfo(sink_info[0]))

    @_eof_cb
    def source_output_cb(self, c, source_output_info, eof, userdata):
        self.data.append(PulseSourceOutputInfo(source_output_info[0]))

    @_eof_cb
    def source_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 disconnect(self):
        self.connected = False
        pa_context_disconnect(self.context)
        pa_context_unref(self.context)
        pa_threaded_mainloop_stop(self.mainloop)
        pa_threaded_mainloop_free(self.mainloop)

    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()

    def sink_input_list(self):
        pa_threaded_mainloop_lock(self.mainloop)
        self.operation = pa_context_get_sink_input_info_list(self.context, self.pa_cbs['sink_input_info'], None)
        self.wait_and_unlock()
        data, self.data = self.fill_clients(), []
        return data or []

    def source_output_list(self):
        pa_threaded_mainloop_lock(self.mainloop)
        self.operation = pa_context_get_source_output_info_list(self.context, self.pa_cbs['source_output_info'], None)
        self.wait_and_unlock()
        data, self.data = self.fill_clients(), []
        return data or []

    @_action_sync
    def sink_list(self):
        self.operation = pa_context_get_sink_info_list(self.context, self.pa_cbs['sink_info'], None)

    @_action_sync
    def source_list(self):
        self.operation = pa_context_get_source_info_list(self.context, self.pa_cbs['source_info'], None)

    @_action_sync
    def get_server_info(self):
        self.operation = pa_context_get_server_info(self.context, self.pa_cbs['server_info'], None)

    @_action_sync
    def card_list(self):
        self.operation = pa_context_get_card_info_list(self.context, self.pa_cbs['card_info'], None)

    @_action_sync
    def client_list(self):
        self.operation = pa_context_get_client_info_list(self.context, self.pa_cbs['client_info'], 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: {}, 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: {}, Name: {}, Mute: {}, {}".format(self.index, self.client.name.decode(), self.mute, self.volume)
        return "ID: {}, 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: {}, 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

    def __str__(self):
        if self.client:
            return "ID: {}, Name: {}, Mute: {}, {}".format(self.index, self.client.name.decode(), self.mute, self.volume)
        return "ID: {}, 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('|', ' '))
        self.index = pa.index
        self.owner = -1
        self.stream_index = -1
        self.media_name = ''
        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
        try:
            media_name = pa.media_name.decode().replace('|', ' ')
            if self.name != media_name:
                self.media_name = ': {}'.format(media_name)
        except:
            self.media_name = ''
        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 = {(91, 49, 59, 50, 67): curses.KEY_SRIGHT,
                  (91, 49, 59, 50, 68): curses.KEY_SLEFT,
                  (79, 80, -1, -1, -1): curses.KEY_F1,
                  (79, 81, -1, -1, -1): curses.KEY_F2,
                  (79, 82, -1, -1, -1): curses.KEY_F3,
                  (79, 72, -1, -1, -1): curses.KEY_HOME,
                  (79, 70, -1, -1, -1): curses.KEY_END,
                  (91, 49, 126, -1, -1): curses.KEY_HOME,
                  (91, 52, 126, -1, -1): 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 - 1, curses.COLS - 1
        self.info, self.menu = str, str
        self.menu_titles = ['F1 Output', 'F2 Input', 'F3 Cards']
        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()
        return self.screen.getch()

    def pregetcher(self):
        # curses.getch doesn't work well with threads
        # so have to improvise ¯\_(ツ)_/¯
        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 next_mode(self):
        for mode, active in self.mode.items():
            if active == 1:
                self.change_mode((1 + mode) % 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 = self.focus_line_num + self.top_line_num + 1
        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]
        pos = int(focus * 100 / self.n_lines)
        name = '{}: {}'.format(bar.fullname, side)
        if len(name) > self.cols - 14:
            name = '{}: {}'.format(bar.fullname[:self.cols - (18 + len(side))], side)
        self.info = '{}|{}\n{}|{}\n{}|{}\n'.format(
            "L ", self.red if bar.locked else curses.A_DIM,
            "M  ", self.red if bar.muted else curses.A_DIM, name, curses.A_NORMAL)
        self.info += '{:>{}}%|{}'.format(pos, self.cols - len(name) - 6, curses.A_BOLD)

    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):
        size = os.get_terminal_size()
        curses.LINES, curses.COLS = size.lines, size.columns
        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 = curses.LINES - 2
        self.cols = curses.COLS - 1
        self.ev.set()

    def termiate(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.2)
        PULSE.subscribe(self.wake_cb)
        self.ev.set()

    def run(self, _):
        signal.signal(signal.SIGINT, lambda s, f: self.termiate())
        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()
                    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
            elif c == 27:
                # translating key-sequences to keys recognized by curses
                # happens in screen/tmux or with TERM=screen/tmux
                c = self.SEQ_TO_KEY.get(tuple([self.screen.getch() for _ in range(5)]), 27)

            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.next_mode()
            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]:
            self.data = self.build(self.modes_data[1][0], PULSE.source_list(), PULSE.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)
            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 = '*'
                name = '{}{}'.format(bar.name, bar.media_name)
                name = name[:20 + off] + '~' if len(name) > 20 + off else name
                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 display_helpwin(self):
        '''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
        F1 F2 F3                Change modes
        Tab                     Go to next mode
        Mouse left click        Select device or mode
        Mouse wheel             Volume change
        Esc q                   Quit'''
        self.helpwin.erase()
        for i, s in enumerate(self.display_helpwin.__doc__.split('\n')):
            self.helpwin.addstr(i + 1, 2, s.strip(), curses.A_NORMAL)
        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
                if type(self.selected[0].pa) is PulseSinkInfo:
                    PULSE.set_sink_port(index, self.selected[0].pa.ports[focus].name)
                elif type(self.selected[0].pa) is PulseSourceInfo:
                    PULSE.set_source_port(index, self.selected[0].pa.ports[focus].name)
                elif type(self.selected[0].pa) is PulseCard:
                    PULSE.set_card_profile(index, self.selected[0].pa.profiles[focus].name)
            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}
        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']]
            mute        = [ord('m')]
            lock        = [ord(' ')]
            quit        = [ord('q'), self._more_keys['KEY_ESC']]

        class UI:
            hide_unavailable_profiles = False
            hide_unavailable_ports = False
            # hide_live = False
            # blip_timeout = 5
            # cycle = False
            color = 2
            mouse = True

        class Style:
            _bar_style = os.getenv('PULSEMIXER_BAR_STYLE', '┌╶┐╴└┘▮- ──').ljust(11, '?')
            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_off            = _bar_style[7]
            arrow              = _bar_style[8]
            arrow_focused      = _bar_style[9]
            arrow_locked       = _bar_style[10]

        self.general = General()
        self.keys = Keys()
        self.ui = UI()
        self.style = Style()

        self.path = os.getenv("XDG_CONFIG_HOME", os.path.join(os.path.expanduser("~"), ".config")) + '/pulsemixer.cfg'
        try:
            self.load()
        except Exception as e:
            sys.exit('ERR: {}'.format(e))

    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
        ; 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
        ;; Keep in mind that vte-based terminals might have problems displaying wide unicode symbols
        ;;      https://bugzilla.gnome.org/show_bug.cgi?id=772890
        ; bar-top-left       = ┌
        ; bar-left-mono      = ╶
        ; bar-top-right      = ┐
        ; bar-right-mono     = ╴
        ; bar-bottom-left    = └
        ; bar-bottom-right   = ┘
        ; bar-on             = ▮
        ; bar-off            = -
        ; arrow              = ' '
        ; arrow-focused      = ─
        ; arrow-locked       = ─
        '''
        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=('#', ';'))
        if not parser.read(self.path): return
        def getkeys(s, k):
            keys = []
            for i in parser.get(s, k).strip(',').split(','):
                i = i.strip().strip('"\'')  # in case 'key' is encountered
                key = getattr(curses, i, self._more_keys.get(i)) if len(i) > 1 else 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(),
               list: getkeys, bool: parser.getboolean,
               int: parser.getint, float: parser.getfloat}
        for section in parser.sections():
            for key in parser[section]:
                pykey = key.replace('-', '_')
                pyval = getattr(getattr(self, section.lower()), pykey)
                val = get[type(pyval)](section, key)
                setattr(getattr(self, section.lower()), pykey, val)


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=", "get-volume",
             "get-mute", "toggle-mute", "mute", "unmute", "create-config",
             "color=", "server=", "no-mouse"])
    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
    CFG = Config()
    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')
    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 sys.stdout.isatty():
            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)
        else:
            sys.exit('ERR: output is not a tty-like device')

    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()

    check_n = lambda x, err: x.strip('+-').isdigit() or sys.exit('ERR: {} must be a number'.format(err))
    if '--id' in dopts:
        index = [i for i in opts if '--id' in i][0][1]
        index = check_n(index, err='ID') and int(index)
    else:
        index = [s.index for s in sinks if s.name == server_info.default_sink_name][0]
    streams = {}
    for i in source_outputs + sources + sink_inputs + sinks:
        streams[i.index] = i
    check_id = lambda x: x in streams or sys.exit('ERR: No such ID: ' + str(x))
    check_id(index)
    print_default = lambda x, y: print(x == y and ', Default' or '')

    for opt, arg in opts:
        if opt == '--id':
            check_n(arg, err='ID')
            index = int(arg)
            check_id(index)

        elif opt in ('-l', '--list'):
            for sink in sink_inputs:
                print("Sink input:\t", sink)
            for source in source_outputs:
                print("Source output:\t", source)
            for sink in sinks:
                print("Sink:\t\t", sink, end='')
                print_default(sink.name, server_info.default_sink_name)
            for source in sources:
                print("Source:\t\t", source, end='')
                print_default(source.name, server_info.default_source_name)

        elif opt == '--list-sinks':
            for sink in sink_inputs:
                print("Sink input:\t", sink)
            for sink in sinks:
                print("Sink:\t\t", sink, end='')
                print_default(sink.name, server_info.default_sink_name)

        elif opt == '--list-sources':
            for source in source_outputs:
                print("Source output:\t", source)
            for source in sources:
                print("Source:\t\t", source, end='')
                print_default(source.name, server_info.default_source_name)

        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] += int(arg)
            PULSE.set_volume(streams[index], vol)


if __name__ == '__main__':
    main()
