github.com/noriah/catnip@v1.8.5/input/pipewire/link.go (about) 1 package pipewire 2 3 import ( 4 "bufio" 5 "context" 6 "fmt" 7 "os" 8 "os/exec" 9 "strconv" 10 "strings" 11 12 "github.com/pkg/errors" 13 ) 14 15 func pwLink(outPortID, inPortID pwObjectID) error { 16 cmd := exec.Command("pw-link", "-L", fmt.Sprint(outPortID), fmt.Sprint(inPortID)) 17 if err := cmd.Run(); err != nil { 18 var exitErr *exec.ExitError 19 if errors.As(err, &exitErr) && exitErr.Stderr != nil { 20 return errors.Wrapf(err, "failed to run pw-link: %s", exitErr.Stderr) 21 } 22 return err 23 } 24 return nil 25 } 26 27 type pwLinkObject struct { 28 DeviceName string 29 PortID pwObjectID 30 PortName string // usually like {input,output}_{FL,FR} 31 } 32 33 func pwLinkObjectParse(line string) (pwLinkObject, error) { 34 var obj pwLinkObject 35 36 idStr, portStr, ok := strings.Cut(line, " ") 37 if !ok { 38 return obj, fmt.Errorf("failed to parse pw-link object %q", line) 39 } 40 41 id, err := strconv.Atoi(idStr) 42 if err != nil { 43 return obj, errors.Wrapf(err, "failed to parse pw-link object id %q", idStr) 44 } 45 46 name, port, ok := strings.Cut(portStr, ":") 47 if !ok { 48 return obj, fmt.Errorf("failed to parse pw-link port string %q", portStr) 49 } 50 51 obj = pwLinkObject{ 52 PortID: pwObjectID(id), 53 DeviceName: name, 54 PortName: port, 55 } 56 57 return obj, nil 58 } 59 60 type pwLinkType string 61 62 const ( 63 pwLinkInputPorts pwLinkType = "i" 64 pwLinkOutputPorts pwLinkType = "o" 65 ) 66 67 type pwLinkEvent interface { 68 pwLinkEvent() 69 } 70 71 type pwLinkAdd pwLinkObject 72 type pwLinkRemove pwLinkObject 73 74 func (pwLinkAdd) pwLinkEvent() {} 75 func (pwLinkRemove) pwLinkEvent() {} 76 77 func pwLinkMonitor(ctx context.Context, typ pwLinkType, ch chan<- pwLinkEvent) error { 78 cmd := exec.CommandContext(ctx, "pw-link", "-mI"+string(typ)) 79 cmd.Stderr = os.Stderr 80 81 o, err := cmd.StdoutPipe() 82 if err != nil { 83 return errors.Wrap(err, "failed to get stdout pipe") 84 } 85 defer o.Close() 86 87 if err := cmd.Start(); err != nil { 88 return errors.Wrap(err, "pw-link -m") 89 } 90 91 scanner := bufio.NewScanner(o) 92 for scanner.Scan() { 93 line := scanner.Text() 94 if line == "" { 95 continue 96 } 97 98 mark := line[0] 99 100 line = strings.TrimSpace(line[1:]) 101 102 obj, err := pwLinkObjectParse(line) 103 if err != nil { 104 continue 105 } 106 107 var ev pwLinkEvent 108 switch mark { 109 case '=': 110 fallthrough 111 case '+': 112 ev = pwLinkAdd(obj) 113 case '-': 114 ev = pwLinkRemove(obj) 115 default: 116 continue 117 } 118 119 select { 120 case <-ctx.Done(): 121 return ctx.Err() 122 case ch <- ev: 123 } 124 } 125 126 return errors.Wrap(cmd.Wait(), "pw-link exited") 127 }