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