Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Widget to toggle mute of pulseaudio default source #87

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ An application to control your Elgato Stream Deck on Linux
- Weather
- Command output
- Recently used windows (X11-only)
- Mute/Unmute default microphone (pulseaudio-only)
- Lets you trigger several actions:
- Run commands
- Emulate a key-press
Expand Down Expand Up @@ -197,6 +198,22 @@ activates the window.
If `showTitle` is `true`, the title of the window will be displayed below the
window icon.

#### Recent Window (requires pulseaudio)

Displays the mute status of the default pulseaudio source. Pressing the button
toggles the state

```toml
[keys.widget]
id = "mute"
[keys.widget.config]
icon = "assets/microphone-rec.png"
iconMute = "assets/microphone-mute.png"
```

If `showTitle` is `true`, the title of the window will be displayed below the
window icon.

#### Time

A flexible widget that can display the current time or date.
Expand Down
9 changes: 9 additions & 0 deletions deck.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,15 @@ func (d *Deck) updateWidgets() {
}
}

// closeWidgets closes all widgets
func (d *Deck) closeWidgets() {
for _, w := range d.Widgets {
if err := w.Close(); err != nil {
fmt.Fprintf(os.Stderr, "Failed to close widget: %s\n", err)
}
}
}

// adjustBrightness adjusts the brightness.
func (d *Deck) adjustBrightness(dev *streamdeck.Device, value string) {
if len(value) == 0 {
Expand Down
Binary file added decks/assets/microphone-mute.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added decks/assets/microphone-rec.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions decks/main.deck
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@
[[keys]]
index = 14
[keys.widget]
id = "recentWindow"
id = "mute"
[keys.widget.config]
window = 5
showTitle = true
icon = "assets/microphone-rec.png"
iconMute = "assets/microphone-mute.png"
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240
github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66
github.com/jfreymuth/pulse v0.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/mitchellh/go-homedir v1.1.0
github.com/muesli/streamdeck v0.2.3-0.20220205132636-dbbc8865ab8c
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 h1:dy+DS31tGEGCsZzB45HmJ
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240/go.mod h1:3P4UH/k22rXyHIJD2w4h2XMqPX4Of/eySEZq9L6wqc4=
github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66 h1:+wPhoJD8EH0/bXipIq8Lc2z477jfox9zkXPCJdhvHj8=
github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66/go.mod h1:KACeV+k6b+aoLTVrrurywEbu3UpqoQcQywj4qX8aQKM=
github.com/jfreymuth/pulse v0.1.0 h1:KN38/9hoF9PJvP5DpEVhMRKNuwnJUonc8c9ARorRXUA=
github.com/jfreymuth/pulse v0.1.0/go.mod h1:cpYspI6YljhkUf1WLXLLDmeaaPFc3CnGLjDZf9dZ4no=
github.com/karalabe/hid v1.0.1-0.20190806082151-9c14560f9ee8 h1:AP5krei6PpUCFOp20TSmxUS4YLoLvASBcArJqM/V+DY=
github.com/karalabe/hid v1.0.1-0.20190806082151-9c14560f9ee8/go.mod h1:Vr51f8rUOLYrfrWDFlV12GGQgM5AT8sVh+2fY4MPeu8=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
Expand Down
68 changes: 68 additions & 0 deletions pulseaudio.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package main

import (
"fmt"
"net"
"os"
"path"

"github.com/jfreymuth/pulse/proto"
pulse "github.com/jfreymuth/pulse/proto"
)

type Source = pulse.GetSourceInfoReply

type PulseAudioClient struct {
client *pulse.Client
conn net.Conn
}

// NewPulseAudioClient returns a new PulseAudioClient.
func NewPulseAudioClient() (*PulseAudioClient, error) {
client, conn, err := pulse.Connect("")
if err != nil {
return nil, err
}

props := pulse.PropList{
"application.name": pulse.PropListString(path.Base(os.Args[0])),
"application.process.id": pulse.PropListString(fmt.Sprintf("%d", os.Getpid())),
"application.process.binary": pulse.PropListString(os.Args[0]),
}
err = client.Request(&pulse.SetClientName{Props: props}, &pulse.SetClientNameReply{})

if err != nil {
conn.Close()
return nil, err
}

return &PulseAudioClient{
client: client,
conn: conn,
}, nil
}

// Close closes the connection to pulseaudio.
func (c *PulseAudioClient) Close() error {
return c.conn.Close()
}

// DefaultSource returns the default source.
func (c *PulseAudioClient) DefaultSource() (*Source, error) {
var source Source
err := c.client.Request(&proto.GetSourceInfo{SourceIndex: proto.Undefined}, &source)
if err != nil {
return nil, err
}
return &source, nil
}

// SetSourceMute set the mute state of a source.
func (c *PulseAudioClient) SetSourceMute(source *Source, mute bool) error {
verbosef("Setting pulseaudio source %s mute to %t", source.SourceName, mute)
err := c.client.Request(&proto.SetSourceMute{SourceIndex: source.SourceIndex, Mute: mute}, nil)
if err != nil {
return err
}
return nil
}
10 changes: 10 additions & 0 deletions widget.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Widget interface {
Action() *ActionConfig
ActionHold() *ActionConfig
TriggerAction(hold bool)
Close() error
}

// BaseWidget provides common functionality required by all widgets.
Expand Down Expand Up @@ -62,6 +63,12 @@ func (w *BaseWidget) TriggerAction(_ bool) {
// just a stub
}

// Close gets called when a button is unloaded.
func (w *BaseWidget) Close() error {
// just a stub
return nil
}

// RequiresUpdate returns true when the widget wants to be repainted.
func (w *BaseWidget) RequiresUpdate() bool {
if !w.lastUpdate.IsZero() && // initial paint done
Expand Down Expand Up @@ -124,6 +131,9 @@ func NewWidget(dev *streamdeck.Device, base string, kc KeyConfig, bg image.Image

case "weather":
return NewWeatherWidget(bw, kc.Widget)

case "mute":
return NewMicrophoneMuteWidget(bw, kc.Widget)
}

// unknown widget ID
Expand Down
117 changes: 117 additions & 0 deletions widget_microphone_mute.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package main

import (
"fmt"
"image"
"os"
)

// MicrophoneMuteWidget is a widget displaying if the default microphone is muted.
type MicrophoneMuteWidget struct {
*ButtonWidget
pulse *PulseAudioClient
mute bool
iconUnmute image.Image
iconMute image.Image
}

// NewMicrophoneMuteWidget returns a new MicrophoneMuteWidget.
func NewMicrophoneMuteWidget(bw *BaseWidget, opts WidgetConfig) (*MicrophoneMuteWidget, error) {
widget, err := NewButtonWidget(bw, opts)
if err != nil {
return nil, err
}

var iconUnmutePath, iconMutePath string
_ = ConfigValue(opts.Config["icon"], &iconUnmutePath)
_ = ConfigValue(opts.Config["iconMute"], &iconMutePath)
iconUnmute, err := preloadImage(widget.base, iconUnmutePath)
if err != nil {
return nil, err
}
iconMute, err := preloadImage(widget.base, iconMutePath)
if err != nil {
return nil, err
}

pulse, err := NewPulseAudioClient()
if err != nil {
return nil, err
}

source, err := pulse.DefaultSource()
if err != nil {
return nil, err
}

return &MicrophoneMuteWidget{
ButtonWidget: widget,
pulse: pulse,
mute: source.Mute,
iconUnmute: iconUnmute,
iconMute: iconMute,
}, nil
}

// RequiresUpdate returns true when the widget wants to be repainted.
func (w *MicrophoneMuteWidget) RequiresUpdate() bool {
source, err := w.pulse.DefaultSource()
if err != nil {
fmt.Fprintf(os.Stderr, "Can't set pulseaudio default source mute: %s\n", err)
return false
}

return w.mute != source.Mute || w.BaseWidget.RequiresUpdate()
}

// Update renders the widget.
func (w *MicrophoneMuteWidget) Update() error {
source, err := w.pulse.DefaultSource()
if err != nil {
return err
}

if w.mute != source.Mute {
w.mute = source.Mute

if w.mute {
w.SetImage(w.iconMute)
} else {
w.SetImage(w.iconUnmute)
}
}

return w.ButtonWidget.Update()
}

// TriggerAction gets called when a button is pressed.
func (w *MicrophoneMuteWidget) TriggerAction(hold bool) {
source, err := w.pulse.DefaultSource()
if err != nil {
fmt.Fprintf(os.Stderr, "Can't get pulseaudio default source: %s\n", err)
return
}
err = w.pulse.SetSourceMute(source, !source.Mute)
if err != nil {
fmt.Fprintf(os.Stderr, "Can't set pulseaudio default source mute: %s\n", err)
return
}
}

// Close gets called when a button is unloaded.
func (w *MicrophoneMuteWidget) Close() error {
return w.pulse.Close()
}

func preloadImage(base string, path string) (image.Image, error) {
path, err := expandPath(base, path)
if err != nil {
return nil, err
}
icon, err := loadImage(path)
if err != nil {
return nil, err
}

return icon, nil
}