Files
2026-08-13 18:12:44 +02:00

63 lines
1.7 KiB
Go

// Package audio wraps wpctl (and the pw-play/pw-cat helper tools that ship
// alongside it) to read and control PipeWire audio state: output/input
// devices, their volume and mute, the default device, and per-application
// streams. Every exported function shells out to a CLI tool; there is no
// direct PipeWire/WirePlumber library integration.
package audio
import (
"bytes"
"context"
"errors"
"fmt"
"os/exec"
"strings"
"time"
)
var ErrNotFound = errors.New("wpctl not found in PATH")
const defaultTimeout = 15 * time.Second
// CheckAvailable reports whether wpctl is installed and PipeWire is
// reachable. Call it before using the rest of the package.
func CheckAvailable() error {
if _, err := exec.LookPath("wpctl"); err != nil {
return ErrNotFound
}
if _, err := run("status"); err != nil {
return err
}
return nil
}
// Installed reports whether wpctl is present in PATH, without probing
// PipeWire. Cheap enough to call when deciding whether to show the Audio
// page in the sidebar.
func Installed() bool {
_, err := exec.LookPath("wpctl")
return err == nil
}
func run(args ...string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
cmd := exec.CommandContext(ctx, "wpctl", args...) // #nosec G204 -- args are fixed wpctl subcommands or IDs read back from `wpctl status`, never shell-interpreted
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
if errors.Is(err, exec.ErrNotFound) {
return "", ErrNotFound
}
msg := strings.TrimSpace(stderr.String())
if msg == "" {
msg = err.Error()
}
return "", fmt.Errorf("wpctl %s: %s", strings.Join(args, " "), msg)
}
return stdout.String(), nil
}