summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--LICENSE24
-rw-r--r--Makefile25
-rw-r--r--README.md97
-rw-r--r--main.c222
5 files changed, 370 insertions, 0 deletions
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 <mitja.felicijan@gmail.com>
+
+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 <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <getopt.h>
+
+#include <pulse/pulseaudio.h>
+
+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;
+}