Skip to content
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
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,26 @@ corresponding icons with correct names need to be placed in
`~/.local/share/deckmaster/themes/[theme]`. The default icons with their
respective names can be found [here](https://github.com/muesli/deckmaster/tree/master/assets/weather).

#### Media player

A widget that displays media player information.

```toml
[keys.widget]
id = "mediaPlayer"
[keys.widget.config]
mode = "art" # optional
player_name = true # optional
icon_playing = "images/stop.jpg"
icon_paused = "images/play.jpg"
icon_no_player = "images/no-player.jpg"
```

The supported modes are `art` (default) and `playback status`. If `playback
status` has been chosen, the icons `icon_playing`, `icon_paused` or
`icon_no_player` will be shown depending on the playback status of the selected
media player.

### Actions

You can hook up any key with several actions. A regular keypress will trigger
Expand Down Expand Up @@ -369,6 +389,29 @@ pressed:
device = "sleep"
```

#### Media player actions

Control playback:

```toml
[keys.action]
media_player = "play_pause"
```

Supported playback actions are `play`, `play_pause`, `stop`, `previous` and
`next`.

Cycle through players:

```toml
[keys.action]
media_player = "select+"
```

Use `select+` to select the next player to be active and `select-` to select
the previous player.


### Background Image

You can configure each deck to display an individual wallpaper behind its
Expand Down
13 changes: 7 additions & 6 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ type DBusConfig struct {

// ActionConfig describes an action that can be triggered.
type ActionConfig struct {
Deck string `toml:"deck,omitempty"`
Keycode string `toml:"keycode,omitempty"`
Exec string `toml:"exec,omitempty"`
Paste string `toml:"paste,omitempty"`
Device string `toml:"device,omitempty"`
DBus DBusConfig `toml:"dbus,omitempty"`
Deck string `toml:"deck,omitempty"`
Keycode string `toml:"keycode,omitempty"`
Exec string `toml:"exec,omitempty"`
Paste string `toml:"paste,omitempty"`
Device string `toml:"device,omitempty"`
DBus DBusConfig `toml:"dbus,omitempty"`
MediaPlayer string `toml:"media_player,omitempty"`
}

// WidgetConfig describes configuration data for widgets.
Expand Down
39 changes: 38 additions & 1 deletion deck.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"time"

"github.com/atotto/clipboard"
"github.com/godbus/dbus"
"github.com/godbus/dbus/v5"
"github.com/muesli/streamdeck"
)

Expand Down Expand Up @@ -264,6 +264,43 @@ func (d *Deck) triggerAction(dev *streamdeck.Device, index uint8, hold bool) {
fmt.Fprintln(os.Stderr, "Unrecognized special action:", a.Device)
}
}
if a.MediaPlayer != "" {
switch {
case a.MediaPlayer == "play":
if player := mediaPlayers.ActivePlayer(); player != nil {
player.Play()
}

case a.MediaPlayer == "play_pause":
if player := mediaPlayers.ActivePlayer(); player != nil {
player.PlayPause()
}

case a.MediaPlayer == "stop":
if player := mediaPlayers.ActivePlayer(); player != nil {
player.Stop()
}

case a.MediaPlayer == "previous":
if player := mediaPlayers.ActivePlayer(); player != nil {
player.Previous()
}

case a.MediaPlayer == "next":
if player := mediaPlayers.ActivePlayer(); player != nil {
player.Next()
}

case a.MediaPlayer == "select+":
mediaPlayers.SelectPlayer(1)

case a.MediaPlayer == "select-":
mediaPlayers.SelectPlayer(-1)

default:
fmt.Fprintln(os.Stderr, "Unrecognized media player action:", a.MediaPlayer)
}
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ require (
github.com/bendahl/uinput v1.5.1
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/flopp/go-findfont v0.1.0
github.com/godbus/dbus v4.1.0+incompatible
github.com/godbus/dbus/v5 v5.1.0
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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ github.com/flopp/go-findfont v0.1.0 h1:lPn0BymDUtJo+ZkV01VS3661HL6F4qFlkhcJN55u6
github.com/flopp/go-findfont v0.1.0/go.mod h1:wKKxRDjD024Rh7VMwoU90i6ikQRCr+JTHB5n4Ejkqvw=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
Expand Down
92 changes: 92 additions & 0 deletions image_downloader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package main

import (
"errors"
"image"
"net/http"
"sync"
"time"
)

type ImageDownloader struct {
ttl time.Duration

cache map[string]CachedImage
mutex *sync.RWMutex
}

type CachedImage struct {
Expires time.Time
Image image.Image
}

func NewImageDownloader(ttl time.Duration) *ImageDownloader {
return &ImageDownloader{
ttl: ttl,

cache: make(map[string]CachedImage),
mutex: &sync.RWMutex{},
}
}

func (d *ImageDownloader) Watch() {
go func() {
time.Sleep(1 * time.Minute)

now := time.Now()
d.mutex.Lock()
for url, c := range d.cache {
if now.After(c.Expires) {
verbosef("removing expired image %s from cache", url)
delete(d.cache, url)
}
}
d.mutex.Unlock()
}()
}

func (d *ImageDownloader) Download(url string) (image.Image, error) {
cachedImage := d.fromCache(url)
if cachedImage != nil {
return cachedImage, nil
}

response, err := http.Get(url)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != 200 {
return nil, errors.New("unable to download image from URL")
}
img, _, err := image.Decode(response.Body)
if err != nil {
return nil, err
}

d.mutex.Lock()
d.cache[url] = CachedImage{
Expires: time.Now().Add(d.ttl),
Image: img,
}
d.mutex.Unlock()

verbosef("saving image %s in cache", url)

return img, nil
}

func (d *ImageDownloader) fromCache(url string) image.Image {
d.mutex.RLock()
defer d.mutex.RUnlock()

if cache, ok := d.cache[url]; ok {
if time.Now().Before(cache.Expires) {
verbosef("returning image %s from cache", url)
return cache.Image
}
verbosef("cache expired for image %s", url)
}

return nil
}
36 changes: 33 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"time"

"github.com/bendahl/uinput"
"github.com/godbus/dbus"
"github.com/godbus/dbus/v5"
"github.com/mitchellh/go-homedir"
"github.com/muesli/streamdeck"
)
Expand All @@ -35,6 +35,10 @@ var (
xorg *Xorg
recentWindows []Window

mediaPlayers *MediaPlayers

imageDownloader *ImageDownloader

deckFile = flag.String("deck", "main.deck", "path to deck config file")
device = flag.String("device", "", "which device to use (serial number)")
brightness = flag.Uint("brightness", 80, "brightness in percent")
Expand Down Expand Up @@ -81,7 +85,7 @@ func expandPath(base, path string) (string, error) {
return filepath.Abs(path)
}

func eventLoop(dev *streamdeck.Device, tch chan interface{}) error {
func eventLoop(dev *streamdeck.Device, tch chan interface{}, mch chan interface{}) error {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

Expand Down Expand Up @@ -145,6 +149,18 @@ func eventLoop(dev *streamdeck.Device, tch chan interface{}) error {
handleActiveWindowChanged(dev, event)
}

case e := <-mch:
switch event := e.(type) {
case MediaPlayerStatusChanged:
fmt.Fprintf(os.Stderr, "Media player event: %T %+v\n", event, event)
handleMediaPlayerStatusChanged()
case ActiveMediaPlayerChanged:
fmt.Fprintf(os.Stderr, "Media player event: %T %+v\n", event, event)
handleMediaPlayerActivePlayerChanged()
default:
fmt.Fprintf(os.Stderr, "Invalid event: %T %+v\n", event, event)
}

case err := <-shutdown:
return err

Expand Down Expand Up @@ -275,14 +291,28 @@ func run() error {
defer keyboard.Close() //nolint:errcheck
}

// initialize mediaPlayer
mch := make(chan interface{}, 20)
mediaPlayers, err = NewMediaPlayers(mch)
if err != nil {
return fmt.Errorf("Error while initializing media players: %s", err)
}
err = mediaPlayers.Run()
if err != nil {
return fmt.Errorf("Error while running media players: %s", err)
}

// initialize image downloader
imageDownloader = NewImageDownloader(1 * time.Hour)

// load deck
deck, err = LoadDeck(dev, ".", *deckFile)
if err != nil {
return fmt.Errorf("Can't load deck: %s", err)
}
deck.updateWidgets()

return eventLoop(dev, tch)
return eventLoop(dev, tch, mch)
}

func main() {
Expand Down
Loading