예제 #1
0
def switch_pulse_default(prop):
    pulse = Pulse('qtile')
    current_default = getattr(pulse.server_info(),
                              'default_{}_name'.format(prop))

    set_next = False
    things = getattr(pulse, '{}_list'.format(prop))()
    for thing in itertools.chain(things, things):
        if set_next:
            notify("Sound", "Default {} to {}".format(prop, thing.name))
            pulse.default_set(thing)
            break
        elif thing.name == current_default:
            set_next = True

    if prop == 'sink':
        list_cmd = 'list-sink-inputs'
        move_cmd = 'move-sink-input'
    else:
        list_cmd = 'list-source-outputs'
        move_cmd = 'move-source-output'

    streams = subprocess.getoutput(
        "pacmd {} | grep index | awk '{{ print $2 }}'".format(list_cmd))
    streams = [int(idx.strip()) for idx in streams.splitlines()]

    for stream in streams:
        cmd = "pacmd {cmd} {stream_id} {thing_id}".format(cmd=move_cmd,
                                                          stream_id=stream,
                                                          thing_id=thing.index)
        print(cmd)
        subprocess.check_call(shlex.split(cmd))
예제 #2
0
파일: __init__.py 프로젝트: dyc3/crystal
class ActionInfoHardware(BaseAction):
    """docstring for ActionInfoHardware."""
    def __init__(self):
        super(ActionInfoHardware, self).__init__()
        self.handled_classifier = "info-hardware"
        self.requires_updater = False

        self.pulse = Pulse('{}-AI'.format("Crystal"))

    #TODO: maybe this should be changed to "info-system" so we can include a lot more info

    @classmethod
    def parse(self, doc) -> (str, list):
        """
		Returns a tuple: query type, parameters (str, list)

		Available query types:
		* processors
		* memory
		* disks
		* audio_devices
		* network_devices
		* battery
		"""
        query_type = None
        query_params = []
        for word in doc:
            if word.dep_ == "nsubj" or word.dep_ == "dobj":
                if word.lemma_ in ["processor", "core", "thread"]:
                    query_type = "processors"
                    if word.lemma_ == "thread":
                        query_params.append("logical")
                        break

                    for child in word.children:
                        if child.dep_ == "amod" and str(child) in [
                                "physical", "logical"
                        ]:
                            query_params.append(str(child))
                            break
                elif word.lemma_ in ["memory", "RAM"]:
                    query_type = "memory"
                elif word.lemma_ in ["disk", "space", "mount"]:
                    query_type = "disks"
                elif word.lemma_ in ["device", "interface"]:
                    for child in word.children:
                        if child.dep_ == "amod":
                            if str(child) in ["audio", "sound"]:
                                query_type = "audio_devices"
                            elif child.lemma_ in ["network", "internet"]:
                                query_type = "network_devices"
                            break
            if word.lemma_ in ["battery", "charge"]:
                query_type = "battery"

        return query_type, query_params

    @classmethod
    def run(self, doc):
        query_type, query_params = self.parse(doc)
        log.info("parsed query: {}, {}".format(query_type, query_params))

        if query_type == None:
            log.error("Failed to parse query_type: {}, {}".format(
                query_type, query_params))
            raise Exception(
                "info-hardware: Failed to parse query_type: {}, {}".format(
                    query_type, query_params))

        if query_type == "processors":
            if len(query_params) == 0:
                feedback.ShowNotify("{} cores, {} threads".format(
                    psutil.cpu_count(logical=False),
                    psutil.cpu_count(logical=True)))
                return
            if "logical" in query_params:
                feedback.ShowNotify("{} threads".format(
                    psutil.cpu_count(logical=True)))
            elif "physical" in query_params:
                feedback.ShowNotify("{} cores".format(
                    psutil.cpu_count(logical=False)))
            else:
                log.error("Unknown query parameters: {}".format(query_params))
        elif query_type == "memory":
            # TODO: do this better, get parameters
            memvirt = psutil.virtual_memory()
            memswap = psutil.swap_memory()
            unit = 1000000000  # gigabyte
            memstring = "Memory Usage: virtual: {} GB/{} GB ({}%) | swap: {} GB/{} GB ({}%)".format(
                round(memvirt.used / unit, 1), round(memvirt.total / unit, 1),
                memvirt.percent, round(memswap.used / unit, 1),
                round(memswap.total / unit, 1), memswap.percent)
            feedback.ShowNotify(memstring)
        elif query_type == "disks":
            parts = psutil.disk_partitions()
            unit = 1000000000  # gigabyte
            partstring = "Disk Usage:"
            for part in parts:
                # ignore these snap package mounts, I don't know why they exist
                if part.mountpoint.startswith("/snap"):
                    continue
                disk = psutil.disk_usage(part.mountpoint)
                partstring += "\n{}: {} GB/{} GB ({}% used)".format(
                    part.mountpoint, round(disk.used / unit, 1),
                    round(disk.total / unit, 1), disk.percent)
            feedback.ShowNotify(partstring)
        elif query_type == "audio_devices":
            # TODO: maybe move this to a seperate "audio" action to handle every thing audio related
            curDefaultSinkName = self.pulse.server_info().default_sink_name
            sinks = self.pulse.sink_list()
            sinks_str = "Sinks:"
            for sink in sinks:
                sinks_str += "\n{}: {} {}".format(
                    sink.index, sink.description,
                    "[DEFAULT]" if sink.name == curDefaultSinkName else "")
            feedback.ShowNotify(sinks_str)

            sources = self.pulse.source_list()
            sources_str = "Sources:"
            for source in sources:
                sources_str += "\n{}: {}".format(source.index,
                                                 source.description)
            feedback.ShowNotify(sources_str)
        elif query_type == "network_devices":
            # doesn't exactly work :/
            net = psutil.net_if_addrs()
            netstring = "Network Interfaces:"
            for interface in net:
                netstring += "\n{}: IPv4: {} IPv6: {} MAC: {}".format(interface, \
                       (str(n) for n in net[interface] if net[interface].family == AddressFamily.AF_INET), \
                       (str(n) for n in net[interface] if net[interface].family == AddressFamily.AF_INET6), \
                       (str(n) for n in net[interface] if net[interface].family == AddressFamily.AF_LINK))
            feedback.ShowNotify(netstring)
        elif query_type == "battery":
            battery = psutil.sensors_battery()
            plugged = "Plugged in" if battery.power_plugged else "Not plugged in"
            percent = str(battery.percent)
            result = "{}: {}%".format(plugged, percent)
            return ActionResponseQuery(result)
예제 #3
0
class PulseMixer(object):
    profiles = {
        "analog": None,
        "a2dp": None,
        "hsp": None,
    }
    current_profile = None
    active_sink = None

    def __init__(self):
        self.pulse = Pulse('volume-control')

    def introspect(self):
        for k in self.profiles.keys():
           self.profiles[k] = None
        self.current_profile = None

        for k in self.profiles.keys():
            self.profiles[k] = None

        self.cards = self.pulse.card_list()
        self.get_active_sink()
        for card in self.cards:
            for profile in card.profile_list:
                if (card.profile_active.name == profile.name
                    and card.name[:4] == self.active_sink.name[:4]):
                    self.current_profile = profile
                key = PROFILE_MAP.get(profile.name, None)
                if key:
                    self.profiles[key] = profile

    def set_profile(self, key):
        # traverse through cards to determine which ones to turn on/off
        next_profile = self.profiles[key]
        next_card = None
        for card in self.cards:
            for profile in card.profile_list:
                if profile == next_profile:
                    next_card = card
                    break
        off_card = None
        off_profile = None
        for card in self.cards:
            if card == next_card:
                continue
            for profile in card.profile_list:
                if profile.name == PROFILE_OFF:
                    off_card = card
                    off_profile = profile
        if not next_card or not next_profile:
            return
        self.pulse.card_profile_set(next_card, next_profile)
        if off_card and off_profile:
            self.pulse.card_profile_set(off_card, off_profile)

    def get_active_sink(self):
        sink = None
        sink_inputs = self.pulse.sink_input_list()
        # check if a sink input is connected to a sink, if no, use default
        if len(sink_inputs):
            sink_id = sink_inputs[0].sink
            sinks = self.pulse.sink_list()
            sink = next((s for s in sinks if s.index == sink_id), None)
        if sink is None:
            info = self.pulse.server_info()
            sink = self.pulse.get_sink_by_name(info.default_sink_name)
        self.active_sink = sink
        return self.active_sink

    def get_sink_volume_and_mute(self):
        mute = True
        volume = 0
        if self.active_sink:
            volume = self.pulse.volume_get_all_chans(self.active_sink)
            volume = min(max(volume, 0), 1) * 100
            mute = self.active_sink.mute
        return volume, mute

    def set_volume(self, value):
        if self.active_sink:
            self.pulse.volume_set_all_chans(self.active_sink, value / 100.0)

    def change_volume(self, value):
        if self.active_sink:
            volume = self.pulse.volume_get_all_chans(self.active_sink)
            volume += value / 100.0
            volume = min(max(volume, 0), 1)
            self.pulse.volume_set_all_chans(self.active_sink, volume)

    def toggle_mute(self):
        sink = self.active_sink
        sink and self.pulse.mute(sink, not sink.mute)

    def start_listener(self, func):
        self.callback = func
        self.thread = threading.Thread(
            target=self.async_listener
        )
        self.thread.daemon = True
        self.thread.start()

    def async_listener(self):
        self.pulse_d = Pulse('volume-daemon')
        self.pulse_d.event_mask_set('sink')

        # Glib.idle_add is to run the callback in the UI thread
        self.pulse_d.event_callback_set(
            lambda e: GLib.idle_add(self.callback)
        )
        self.pulse_d.event_listen()
예제 #4
0
파일: core.py 프로젝트: mishurov/applets
class PulseMixer(object):
    all_profiles = {}
    current_profile = None
    active_sink = None

    def __init__(self):
        self.pulse = Pulse('volume-control')

    def introspect(self):
        self.all_profiles = {}
        self.current_profile = None
        self.cards = self.pulse.card_list()

        for card in self.cards:
            description = card.proplist.get('device.description')
            for profile in card.profile_list:
                #print(profile)
                prof_key = PROFILE_MAP.get(profile.name, None)
                if prof_key and profile.available:
                    key = description + '__' + prof_key
                    self.all_profiles[key] = [card, profile]
                    if (card.profile_active.name == profile.name
                            and self.active_sink
                            and card.name[:4] == self.active_sink.name[:4]):
                        self.current_profile = key

    def set_profile(self, key):
        prof = self.all_profiles[key]
        if not prof:
            return
        card, profile = prof
        for c, p in self.all_profiles.values():
            if c != card:
                self.pulse.card_profile_set(c, PROFILE_OFF)
            elif p == profile:
                self.pulse.card_profile_set(card, profile)

    def get_active_sink(self):
        sink = None
        sink_inputs = self.pulse.sink_input_list()
        # check if a sink input is connected to a sink, if no, use default
        if len(sink_inputs):
            sink_id = sink_inputs[0].sink
            sinks = self.pulse.sink_list()
            sink = next((s for s in sinks if s.index == sink_id), None)
        if sink is None:
            info = self.pulse.server_info()
            if info.default_sink_name == '@DEFAULT_SINK@':
                return None
            sink = self.pulse.get_sink_by_name(info.default_sink_name)
        self.active_sink = sink
        return self.active_sink

    def get_sink_volume_and_mute(self):
        mute = True
        volume = 0
        if self.active_sink:
            volume = self.pulse.volume_get_all_chans(self.active_sink)
            volume = min(max(volume, 0), 1) * 100
            mute = self.active_sink.mute
        return volume, mute

    def set_volume(self, value):
        if self.active_sink:
            self.pulse.volume_set_all_chans(self.active_sink, value / 100.0)

    def change_volume(self, value):
        if self.active_sink:
            volume = self.pulse.volume_get_all_chans(self.active_sink)
            volume += value / 100.0
            volume = min(max(volume, 0), 1)
            self.pulse.volume_set_all_chans(self.active_sink, volume)

    def get_mute(self):
        return self.active_sink.mute

    def toggle_mute(self):
        sink = self.active_sink
        sink and self.pulse.mute(sink, not sink.mute)

    def start_listener(self, func):
        self.callback = func
        self.thread = threading.Thread(target=self.async_listener)
        self.thread.daemon = True
        self.thread.start()

    def async_listener(self):
        self.pulse_d = Pulse('volume-daemon')
        self.pulse_d.event_mask_set('sink', 'card')

        # Glib.idle_add is to run the callback in the UI thread
        self.pulse_d.event_callback_set(self.callback())
        try:
            self.pulse_d.event_listen()
        except PulseDisconnected:
            time.sleep(3)
            self.pulse = Pulse('volume-control')
            self.async_listener()
예제 #5
0
class Feeder(object):
    def __init__(self, i3, bat, bus, bar, display_title=True):
        self.out = ''
        self.datetime = ''
        self.workspaces = dict()
        self.volume = ''
        self.battery = ''
        self.i3 = i3
        self.bar = bar
        self.display_title = display_title
        focused = i3.get_tree().find_focused()
        self.focusedWinTitle = focused.name if not focused.type == 'workspace' and display_title else ''
        self.currentBindingMode = ''
        self.mode = ''
        self.bat = bat
        self.bus = bus
        self.pulse = Pulse('my-client-name')
        self.outputs = ''
        self.notitle = " %%{R B-}%s%%{F-}" % self.bar.sep_left
        self.title = self.notitle

        self.set_outputs()

    def set_outputs(self):
        mon = [[out.name, out.rect.x, out.rect.y]
               for out in self.i3.get_outputs() if out.active]
        quickSort(mon, 0, len(mon) - 1)
        self.outputs = [el[0] for el in mon]

    def on_battery_event(self, device, props, signature):
        perc = round(self.bat.Get(device, "Percentage"))
        state = self.bat.Get(device, "State")
        timeleft = self.bat.Get(device,
                                "TimeToEmpty" if state == 2 else "TimeToFull")
        self.render_battery(perc, state, timeleft)
        for idx, output in enumerate(self.outputs):
            self.display(idx)
        print('')
        #print('bat', file=stderr)

    def on_binding_mode_change(self, caller, e):
        self.currentBindingMode = e.change
        if self.currentBindingMode != "default":
            self.render_binding_mode()
        else:
            self.mode = ''

        for idx, output in enumerate(self.outputs):
            self.display(idx)
        print('')
        #print('bind', file=stderr)

    def on_workspace_focus(self, caller, e):
        self.focusedWinTitle = e.current.find_focused()
        if not self.focusedWinTitle:
            self.title = self.notitle

        self.on_workspace_event(caller, e)
        #print('wksp2', file=stderr)

    def on_workspace_event(self, caller, e):
        for idx, output in enumerate(self.outputs):
            self.render_workspaces(index=idx, display=output)
            self.display(idx)
        print('')
        #print('wksp1', file=stderr)

    def on_timedate_event(self):
        self.render_datetime()

        for idx, output in enumerate(self.outputs):
            self.display(idx)
        print('')
        #print('time', file=stderr)

    def on_window_close(self, caller, e):
        focusedWin = self.i3.get_tree().find_focused()
        if not focusedWin.type == 'workspace':
            self.focusedWinTitle = focusedWin.name
            self.render_focused_title()
            try:
                for idx, output in enumerate(self.outputs):
                    self.display(idx)
                print('')
            except:
                fd = open('/tmp/lemonbar', 'a')
                fd.write('error title change\n')
        else:
            self.title = self.notitle
            for idx, output in enumerate(self.outputs):
                self.display(idx)
            print('')

    def on_window_title_change(self, caller, e):
        self.focusedWinTitle = e.container.name
        self.render_focused_title()
        try:
            for idx, output in enumerate(self.outputs):
                self.display(idx)
            print('')
        except:
            fd = open('/tmp/lemonbar', 'a')
            fd.write('error title change\n')

    def on_volume_event(self, ev):
        default_output_sink_name = self.pulse.server_info().default_sink_name
        sink = self.pulse.get_sink_by_name(default_output_sink_name)
        self.render_volume(sink)

        for idx, output in enumerate(self.outputs):
            self.display(idx)
        print('')
        #print('vol', file=stderr)

    def render_workspaces(self, index, display):
        wsp_icon = "%%{F- B%s} %%{T2}%s%%{T1}" % (self.bar.colors['in_bg'],
                                                  icon_wsp)
        wsp_items = ''
        sep = self.bar.sep_left

        for wsp in self.i3.get_workspaces():
            wsp_name = wsp.name
            wsp_action = "%%{A1:i3-msg -q workspace %s:}" % wsp_name
            if wsp.output != display:  #and not wsp.urgent:
                continue
            if wsp.focused:
                wsp_items += " %%{R B%s}%s%s%%{F%s T1} %s%%{A}" % (
                    self.bar.colors['foc_bg'], sep, wsp_action,
                    self.bar.colors['foc_fg'], wsp_name)
            elif wsp.urgent:
                wsp_items += " %%{R B%s}%s%s%%{F- T1} %s" % (
                    self.bar.colors['ur_bg'], sep, wsp_action, wsp_name)
            else:
                if not wsp_items:
                    wsp_items += "  %s%%{T1} %s%%{A}" % (wsp_action, wsp_name)
                else:
                    wsp_items += " %%{R B%s}%s%s%%{F- T1} %s%%{A}" % (
                        self.bar.colors['in_bg'], sep, wsp_action, wsp_name)

        self.workspaces[index] = '%s%s' % (wsp_icon, wsp_items)

    def render_focused_title(self):
        sep = self.bar.sep_left
        self.title = " %%{R B%s}%s%%{F- T2} %s %%{T1}%s %%{R B-}%s%%{F-}" % (
            self.bar.colors['ti_bg'], sep, icon_prog, self.focusedWinTitle,
            sep)

    def render_binding_mode(self):
        self.mode = " %%{R B%s}%s%%{F%s} %%{T1} %s" % (
            self.bar.colors['bd_bg'], self.bar.sep_left,
            self.bar.colors['bd_fg'], self.currentBindingMode)

    def render_datetime(self):
        sep = self.bar.sep_right
        cdate = " %%{F%s}%s%%{R F-} %%{T2}%s%%{T1} %s" % (
            '#A0' + self.bar.colors['gen_bg'], sep, icon_clock,
            strftime("%d-%m-%Y"))
        ctime = " %%{F%s}%s%%{R F-} %s %%{B-}" % (self.bar.colors['foc_bg'],
                                                  sep, strftime("%H:%M"))
        self.datetime = "%s%s" % (cdate, ctime)

    def render_volume(self, sink):
        value = round(sink.volume.value_flat * 100)
        mute = bool(sink.mute)
        volume = str(value) + '%' if not mute else 'MUTE'
        self.volume = "%%{F%s}%s%%{R F-} %%{T2}%s%%{T1} %s" % (
            '#A0' + self.bar.colors['gen_bg'], self.bar.sep_right, icon_vol,
            volume)

    def render_battery(self, val, state, timeleft):
        value = str(val) + "%"
        remaining = " (%s)" % strftime(
            "%H:%M", gmtime(timeleft)) if timeleft > 0 else ''
        fg = '-'
        bg = self.bar.colors['foc_bg']
        if state == 2:
            if val > 50:
                fg = fg_battery_50
            elif val > 25:
                fg = fg_battery_25
            elif val > 10:
                fg = fg_battery_10
            else:
                bg = bg_battery_0
        self.battery = " %%{F%s}%s%%{R F%s} %s%s" % (bg, self.bar.sep_right,
                                                     fg, value, remaining)

    def render_all(self, caller=None, e=None):
        # Render one bar per each output
        if e == None:
            device = "org.freedesktop.UPower.Device"
            perc = round(self.bat.Get(device, "Percentage"))
            state = self.bat.Get(device, "State")
            timeleft = self.bat.Get(
                device, "TimeToEmpty" if state == 2 else "TimeToFull")
            default_output_sink_name = self.pulse.server_info(
            ).default_sink_name
            sink = self.pulse.get_sink_by_name(default_output_sink_name)

            self.render_battery(perc, state, timeleft)
            self.render_datetime()
            self.render_volume(sink)
            if not self.focusedWinTitle == '': self.render_focused_title()

        for idx, output in enumerate(self.outputs):
            self.render_workspaces(index=idx, display=output)
            self.display(idx)
        print('')

    def display(self, idx):
        #self.render_volume(0)
        self.out = "%%{S%d}%%{l}%s%s%s%%{r}%s%s%s" % (
            idx, self.workspaces[idx], self.mode, self.title, self.volume,
            self.battery, self.datetime)
        print(self.out, end='')
예제 #6
0
class Py3status:
    py3: Py3
    blocks = u"_▁▂▃▄▅▆▇█"
    button_down = 5
    button_mute = 1
    button_up = 4
    format = u"{icon} {percentage}%"
    format_muted = u"{icon} {percentage}%"
    is_input = False
    max_volume = 100
    thresholds = [(0, "good"), (75, "degraded"), (100, "bad")]
    volume_delta = 5

    def __init__(self,
                 sink_name: Optional[str] = None,
                 volume_boost: bool = False):
        """

        :param sink_name:  Sink name to use. Empty uses default sink
        :param volume_boost: Whether to allow setting volume above 1.0 - uses software boost
        """
        self._sink_name = sink_name
        self._sink_info: Optional[PulseSinkInfo]
        self._volume_boost = volume_boost
        self._pulse_connector = Pulse('py3status-pulse-connector',
                                      threading_lock=True)
        self._pulse_connector_lock = threading.Lock()
        self._volume: Optional[Volume] = None
        self._backend_thread = threading.Thread

    def _get_volume_from_backend(self):
        """Get a new sink on every call.

        The sink is not updated when the backed values change.
        Returned volume is the maximum of all available channels.
        """
        sink_name = self._pulse_connector.server_info().default_sink_name
        self._sink_info = self._pulse_connector.get_sink_by_name(sink_name)
        pulse_volume = Volume.from_sink_info(self._sink_info)
        logger.debug(pulse_volume)
        if self._volume != pulse_volume:
            self._volume = pulse_volume
            self.py3.update()

    def _callback(self, ev):
        if ev.t == PulseEventTypeEnum.change and \
                (ev.facility == PulseEventFacilityEnum.server or
                 ev.facility == PulseEventFacilityEnum.sink and ev.index == self._sink_info.index):
            raise PulseLoopStop

    def _pulse_reader(self):
        while True:
            try:
                self._pulse_connector.event_listen()
                self._get_volume_from_backend()
            except PulseDisconnected:
                logger.debug("Pulse disconnected. Stopping reader.")
                break

    def post_config_hook(self):
        self._pulse_connector.connect()
        self._get_volume_from_backend()
        self._pulse_connector.event_mask_set(PulseEventMaskEnum.server,
                                             PulseEventMaskEnum.sink)
        self._pulse_connector.event_callback_set(self._callback)
        self._backend_thread = threading.Thread(
            name="pulse_backend", target=self._pulse_reader).start()

    def kill(self):
        logger.info("Shutting down")
        self._pulse_connector.disconnect()

    def _color_for_output(self) -> str:
        if self._volume is None:
            return self.py3.COLOR_BAD
        if self._volume.mute:
            return self.py3.COLOR_MUTED or self.py3.COLOR_BAD
        return self.py3.threshold_get_color(self._volume.level)

    def _icon_for_output(self) -> str:
        return self.blocks[min(
            len(self.blocks) - 1,
            int(math.ceil(self._volume.level / 100 * (len(self.blocks) - 1))),
        )]

    def _format_output(self) -> Union[str, Composite]:
        return self.py3.safe_format(format_string=self.format_muted
                                    if self._volume.mute else self.format,
                                    param_dict={
                                        "icon": self._icon_for_output(),
                                        "percentage": self._volume.level
                                    })

    def volume_status(self):
        response = {
            "cached_until": self.py3.CACHE_FOREVER,
            "color": self._color_for_output(),
            "full_text": self._format_output()
        }
        return response
예제 #7
0
class pulseaudiostuff():
    def __init__(self):
        logging.debug("Initing %s", self.__class__.__name__)
        self.pulse = Pulse()
        self.barecmd = connect_to_cli()

        default_sink_name = self.pulse.server_info().default_sink_name
        self.default_sink_info = self.pulse.get_sink_by_name(default_sink_name)

        out_sink_name = "game-out"
        # music + mic
        self.out_sink_module_id = self.pulse.module_load(
            "module-null-sink", 'sink_name=' + out_sink_name)
        self.out_sink_info = self.pulse.get_sink_by_name(out_sink_name)

        MP_sink_name = "Media-player"
        # that is our main sink. send your media here
        # everything that comes in is being copied to game sink and default sink
        self.MP_sink_module_id = self.pulse.module_load(
            "module-combine-sink", 'sink_name=' + MP_sink_name + ' slaves=' +
            str(self.out_sink_info.index) + ',' +
            str(self.default_sink_info.index))
        self.MP_sink_info = self.pulse.get_sink_by_name(MP_sink_name)

        # Get stream media -> speakers
        # TODO: this is also gay but it is somehow possible to retreve all inputs for sink. (sink_input_list(sinkIndex))
        for stream in self.pulse.sink_input_list():
            if stream.owner_module == self.MP_sink_module_id:
                if stream.sink == self.default_sink_info.index:
                    self.sound2speakers = stream
                elif stream.sink == self.out_sink_info.index:
                    self.sound2game = stream

        # send mic stream to game sink. (btw rip 20 ms)
        self.loopback_module_id = self.pulse.module_load(
            "module-loopback", 'sink=' + str(self.out_sink_info.index) +
            ' latency_msec=20 source_dont_move=true sink_dont_move=true')

        # Get stream mic -> game
        # TODO: this is also gay but it is somehow possible to retreve all inputs for sink. (sink_input_list(sinkIndex))
        for stream in self.pulse.sink_input_list():
            if stream.sink == self.out_sink_info.index and stream.owner_module == self.loopback_module_id:
                self.mic2game = stream

        # TODO: combine sink sets volume to earrape because reasons?
        hell = self.sound2speakers.volume
        hell.value_flat = 0.5
        self.pulse.volume_set(self.sound2speakers, hell)
        hell.value_flat = 1.0
        self.pulse.volume_set(self.sound2game, hell)

        # TODO: change names of sinks.
        # https://gitlab.freedesktop.org/pulseaudio/pulseaudio/-/issues/615
        # self.barecmd.write(
        #     'update-sink-proplist ' + str(self.out_sink_info.index) + ' device.description="Game media player out"')
        # self.barecmd.write(
        #     'update-sink-proplist ' + str(self.MP_sink_info.index) + ' device.description="Game media player sink"')

        self.pulse.sink_default_set(self.default_sink_info)

        logging.debug("%s class loaded", self.__class__.__name__)
        logging.info("out sink module id: %d", self.out_sink_module_id)
        logging.info("MP sink module id: %d", self.MP_sink_module_id)
        logging.info("loopback module id: %d", self.loopback_module_id)

    def __del__(self):
        self.pulse.module_unload(self.loopback_module_id)
        self.pulse.module_unload(self.out_sink_module_id)
        self.pulse.module_unload(self.MP_sink_module_id)
        logging.debug("%s class unloaded", self.__class__.__name__)

    def printstuff(self):
        print('csgo launch options: "pacmd set-default-source ' +
              self.out_sink_info.name + '.monitor"')
def main(args):
    pulse = Pulse("pulse-audio-cycle")

    current_sink = pulse.get_sink_by_name(
        pulse.server_info().default_sink_name)

    current_profile = pulse.card_info(current_sink.card).profile_active

    # card -> holds all profiles and sets the active one
    # sink uses a card+profile combination and is names accordingly

    matching_cards_with_profiles = []
    card_pattern = re.compile(args.card)
    logging.debug(f"card_pattern: {card_pattern}")
    # Get a list of all matching cards
    for card in pulse.card_list():
        # Prepare to also match against sink description
        sink_for_current_card = None
        if args.use_sink_description:
            sink_for_current_card = sink_for_card(card, pulse)

        card_pattern_matched = False
        if re.search(card_pattern, card.name):
            card_pattern_matched = True
            logging.info(f"Card matched: {card.name}")
            if sink_for_current_card:
                logging.info(
                    f"-> Sink Description: {sink_for_current_card.description}"
                )
        elif args.use_sink_description:
            if sink_for_current_card:
                if re.search(card_pattern, sink_for_current_card.description):
                    card_pattern_matched = True
                    logging.info(f"Card matched: {card.name}")
                    logging.info(
                        f"-> Sink Description: {sink_for_current_card.description}"
                    )
                    logging.info("-> matched via Sink Description")
        # Ignore cards that are not wanted by the user given pattern
        if not card_pattern_matched:
            continue

        matched_profiles = []
        # Check if we need filter for certain profiles or leave as is
        for profile in card.profile_list:
            # skip unavailable profiles (unless wanted)
            if not profile.available and not args.with_unavailable:
                continue

            # Check every given profile
            for cp_card_pattern, cp_profile_pattern in args.profile:
                cp_card_pattern_matched = False
                if re.search(cp_card_pattern, card.name):
                    cp_card_pattern_matched = True
                elif args.use_sink_description:
                    if sink_for_current_card:
                        if re.search(cp_card_pattern,
                                     sink_for_current_card.description):
                            cp_card_pattern_matched = True

                # This cp_profile_pattern does not apply to this card
                if not cp_card_pattern_matched:
                    continue

                if re.search(cp_profile_pattern, profile.name):
                    logging.info(f"  Profile matched: {profile.name} ")
                    matched_profiles.append(profile)

        if not matched_profiles:
            logging.info("  No Profile matched – Keeping profile.")

        # put infos into list
        matching_cards_with_profiles.append((card, matched_profiles))

        # separator betweem cards
        logging.info("")

    new_card, new_profile = new_card_and_profile(matching_cards_with_profiles,
                                                 pulse)

    # change profile if necessary
    if new_profile:
        if args.verbose:
            logging.info(f"New Profile: {new_profile.description}")
        if not args.dry:
            pulse.card_profile_set(new_card, new_profile)
    else:
        if args.verbose:
            logging.info("NO new Profile.")

    # change sink (has to be done always because card profile also changes sink)
    new_sink = sink_for_card(new_card, pulse)
    if args.verbose:
        logging.info(f"New Card: {new_card.name}")
        if args.use_sink_description:
            logging.info(f"-> New Sink: {new_sink.description} ")

    if not args.dry:
        pulse.sink_default_set(new_sink)

    # move all input sinks (apps/X clients) to new output sink
    for input_sink in pulse.sink_input_list():
        if args.verbose:
            logging.info(
                f"  -> Switching {input_sink.proplist['application.name']}")
        if not args.dry:
            pulse.sink_input_move(input_sink.index, new_sink.index)

    # Show notification
    if args.notify:
        details = f"New Sink: {new_sink.description}"
        if new_profile:
            details += f"\nNew Profile: {new_profile.description}"

        notify("Sink Changed", details)