From 168716cdb64e27dd1a7a466b772c924be7e97fdc Mon Sep 17 00:00:00 2001 From: Mitja Felicijan Date: Fri, 18 Jul 2025 22:35:14 +0200 Subject: Engage --- .gitignore | 2 + LICENSE | 24 +++++++ Makefile | 25 +++++++ README.md | 97 +++++++++++++++++++++++++++ main.c | 222 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 370 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 main.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ddd8075 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.o +pats diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..23f05ad --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +BSD 2-Clause License + +Copyright (c) 2025, Mitja Felicijan + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9c6cf68 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +CC = gcc +CFLAGS = -Wall -Wextra +LDFLAGS = -lpulse + +TARGET = pats +SOURCE = main.c + +# Installation paths +PREFIX = /usr/local +BINDIR = $(PREFIX)/bin + +$(TARGET): $(SOURCE) + $(CC) $(CFLAGS) -o $(TARGET) $(SOURCE) $(LDFLAGS) + +install: $(TARGET) + install -d $(BINDIR) + install -m 755 $(TARGET) $(BINDIR) + +uninstall: + rm -f $(BINDIR)/$(TARGET) + +clean: + rm -f $(TARGET) + +.PHONY: clean install uninstall diff --git a/README.md b/README.md new file mode 100644 index 0000000..a206992 --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +# PulseAudio Sink Toggle Tool + +Pats is a command-line tool for managing PulseAudio audio sinks that allows you to +list available audio outputs and toggle between them seamlessly. + +## Features + +| Feature | Description | +|---------|-------------| +| **List audio sinks** | Display all available PulseAudio sinks with active status | +| **Toggle between sinks** | Switch between available audio outputs | + +## How to use + +> [!IMPORTANT] +> Make sure you have the required dependencies installed before +> building the project. + +First, ensure you have the necessary development libraries installed: + +```sh +# On Void Linux +sudo xbps-install -S pulseaudio-devel + +# On Debian/Ubuntu +sudo apt-get install libpulse-dev + +# On Fedora/RHEL +sudo dnf install pulseaudio-libs-devel + +# On Arch Linux +sudo pacman -S pulseaudio +``` + +Build the project: + +```sh +make +sudo make install +``` + +### Command line options + +The tool supports several command line options: + +```sh +pats [OPTIONS] + +Options: + -l, --list List all available audio sinks + -t, --toggle Toggle between available audio sinks + -h, --help Show this help message +``` + +### Examples + +List all available audio sinks: +```sh +pats --list +``` + +Toggle to the next available audio sink: +```sh +pats --toggle +``` + +Short option syntax: +```sh +pats -l # List sinks +pats -t # Toggle sinks +``` + +## Building from source + +The project uses a simple Makefile for building. Available targets: + +```sh +make # Build the executable (default) +make clean # Remove build artifacts +sudo make install # Install to /usr/local/bin/ +sudo make uninstall # Remove from /usr/local/bin/ +``` + +### Debugging + +Enable verbose output by modifying the source code or using +debugging tools like `gdb`: + +```sh +gdb ./pats +``` + +## License + +[pats](https://github.com/mitjafelicijan/pats) was written by [Mitja +Felicijan](https://mitjafelicijan.com) and is released under the BSD +two-clause license, see the LICENSE file for more information.. \ No newline at end of file diff --git a/main.c b/main.c new file mode 100644 index 0000000..bc0c303 --- /dev/null +++ b/main.c @@ -0,0 +1,222 @@ +#include +#include +#include +#include +#include + +#include + +static pa_mainloop *mainloop = NULL; +static pa_context *context = NULL; +static int list_mode = 0; +static int toggle_mode = 0; + +static pa_sink_info **sinks = NULL; +static unsigned int sink_count = 0; +static unsigned int current_sink_index = 0; + +void server_info_callback(pa_context *c, const pa_server_info *i, void *userdata); +void set_default_sink_callback(pa_context *c, int success, void *userdata); + +int get_output_type(const char *input, char *output, size_t output_size) { + if (!input || !output || output_size == 0) return -1; + + const char *last_dot = strrchr(input, '.'); + if (!last_dot) return -1; + last_dot++; + + const char *hyphen = strchr(last_dot, '-'); + size_t len = hyphen ? (size_t)(hyphen - last_dot) : strlen(last_dot); + + if (len >= output_size) len = output_size - 1; + + strncpy(output, last_dot, len); + output[len] = '\0'; + + return 0; +} + +void sink_callback(pa_context *c, const pa_sink_info *i, int eol, void *userdata) { + (void)userdata; + + if (eol > 0) { + if (list_mode) { + pa_mainloop_quit(mainloop, 0); + } else if (toggle_mode) { + pa_operation_unref(pa_context_get_server_info(c, server_info_callback, NULL)); + } + return; + } + + if (list_mode) { + int is_active = 0; + if (i->state == PA_SINK_RUNNING) { + is_active = 1; + } + + char output_type[64]; + if (get_output_type(i->name, output_type, sizeof(output_type)) == 0) { + printf(" %s%s", output_type, is_active ? "*" : ""); + } + + } else if (toggle_mode) { + sinks = realloc(sinks, (sink_count + 1) * sizeof(pa_sink_info*)); + sinks[sink_count] = malloc(sizeof(pa_sink_info)); + + memcpy(sinks[sink_count], i, sizeof(pa_sink_info)); + + if (i->name) { + size_t name_len = strlen(i->name) + 1; + char *name_copy = malloc(name_len); + strcpy(name_copy, i->name); + sinks[sink_count]->name = name_copy; + } + + if (i->description) { + size_t desc_len = strlen(i->description) + 1; + char *desc_copy = malloc(desc_len); + strcpy(desc_copy, i->description); + sinks[sink_count]->description = desc_copy; + } + + sink_count++; + } +} + +void set_default_sink_callback(pa_context *c, int success, void *userdata) { + (void)userdata; + + if (success) { + printf("Successfully switched default sink\n"); + } else { + int error = pa_context_errno(c); + fprintf(stderr, "Failed to switch default sink: %s (error code: %d)\n", pa_strerror(error), error); + } + pa_mainloop_quit(mainloop, success ? 0 : 1); +} + +void server_info_callback(pa_context *c, const pa_server_info *i, void *userdata) { + (void)userdata; + + if (!i) { + pa_mainloop_quit(mainloop, 1); + return; + } + + // Find current default sink in our list. + for (unsigned int j = 0; j < sink_count; j++) { + if (strcmp(sinks[j]->name, i->default_sink_name) == 0) { + current_sink_index = j; + break; + } + } + + // Switch to next sink. + unsigned int next_sink_index = (current_sink_index + 1) % sink_count; + printf("Switching from %s to %s\n", + sinks[current_sink_index]->description, + sinks[next_sink_index]->description); + + // Set the new default sink with callback. + pa_operation_unref(pa_context_set_default_sink(c, sinks[next_sink_index]->name, set_default_sink_callback, NULL)); +} + +void state_callback(pa_context *c, void *userdata) { + (void)userdata; + + switch (pa_context_get_state(c)) { + case PA_CONTEXT_READY: + if (list_mode) { + pa_operation_unref(pa_context_get_sink_info_list(c, sink_callback, NULL)); + } else if (toggle_mode) { + pa_operation_unref(pa_context_get_sink_info_list(c, sink_callback, NULL)); + } + break; + case PA_CONTEXT_FAILED: + case PA_CONTEXT_TERMINATED: + pa_mainloop_quit(mainloop, 1); + break; + default: + break; + } +} + +void print_usage(const char *program_name) { + printf("Usage: %s [OPTIONS]\n", program_name); + printf("Options:\n"); + printf(" -l, --list List all available audio sinks\n"); + printf(" -t, --toggle Toggle between available audio sinks\n"); + printf(" -h, --help Show this help message\n"); +} + +int main(int argc, char *argv[]) { + int opt; + const char *short_options = "lth"; + struct option long_options[] = { + {"list", no_argument, 0, 'l'}, + {"toggle", no_argument, 0, 't'}, + {"help", no_argument, 0, 'h'}, + {0, 0, 0, 0} + }; + + while ((opt = getopt_long(argc, argv, short_options, long_options, NULL)) != -1) { + switch (opt) { + case 'l': + list_mode = 1; + break; + case 't': + toggle_mode = 1; + break; + case 'h': + print_usage(argv[0]); + return 0; + default: + print_usage(argv[0]); + return 1; + } + } + + if (!list_mode && !toggle_mode) { + fprintf(stderr, "Error: Please specify either -l (list) or -t (toggle) option\n"); + print_usage(argv[0]); + return 1; + } + + if (list_mode && toggle_mode) { + fprintf(stderr, "Error: Cannot use both -l and -t options simultaneously\n"); + return 1; + } + + mainloop = pa_mainloop_new(); + context = pa_context_new(pa_mainloop_get_api(mainloop), "SinkLister"); + + if (pa_context_connect(context, NULL, PA_CONTEXT_NOFLAGS, NULL) < 0) { + fprintf(stderr, "Failed to connect to PulseAudio server: %s\n", pa_strerror(pa_context_errno(context))); + return 1; + } + + pa_context_set_state_callback(context, state_callback, NULL); + + int retval; + pa_mainloop_run(mainloop, &retval); + + // Cleanup for toggle mode + if (toggle_mode && sinks) { + for (unsigned int i = 0; i < sink_count; i++) { + if (sinks[i]->name) { + free((void*)sinks[i]->name); + } + if (sinks[i]->description) { + free((void*)sinks[i]->description); + } + free(sinks[i]); + } + free(sinks); + } + + pa_context_disconnect(context); + pa_context_unref(context); + pa_mainloop_free(mainloop); + + return retval; +} -- cgit v1.2.3