Skip to content

Commit

Permalink
Added support for loading deck definitions, launching apps and displa…
Browse files Browse the repository at this point in the history
…ying CPU usage
  • Loading branch information
muesli committed Jul 19, 2019
1 parent cd87951 commit b304b47
Show file tree
Hide file tree
Showing 7 changed files with 377 additions and 34 deletions.
63 changes: 63 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package main

import (
"bytes"
"io/ioutil"

"github.com/BurntSushi/toml"
)

type DBusConfig struct {
Object string `toml:"object,omitempty"`
Path string `toml:"path,omitempty"`
Method string `toml:"method,omitempty"`
Value string `toml:"value,omitempty"`
}

type ActionConfig struct {
Keycode string `toml:"keycode,omitempty"`
Exec string `toml:"exec,omitempty"`
Deck string `toml:"deck,omitempty"`
DBus DBusConfig `toml:"dbus,omitempty"`
}

type WidgetConfig struct {
ID string `toml:"id,omitempty"`
Config map[string]string `toml:"config,omitempty"`
}

type KeyConfig struct {
Index uint8 `toml:"index"`
Widget WidgetConfig `toml:"widget"`
Action *ActionConfig `toml:"action,omitempty"`
}
type Keys []KeyConfig

type DeckConfig struct {
Keys Keys `toml:"keys"`
}

// LoadConfig loads config from filename
func LoadConfig(filename string) (DeckConfig, error) {
config := DeckConfig{}

b, err := ioutil.ReadFile(filename)
if err != nil {
return config, err
}

_, err = toml.Decode(string(b), &config)
return config, err
}

// Save writes config as json to filename
func (c DeckConfig) Save(filename string) error {
var b bytes.Buffer
e := toml.NewEncoder(&b)
err := e.Encode(c)
if err != nil {
return err
}

return ioutil.WriteFile(filename, b.Bytes(), 0644)
}
57 changes: 57 additions & 0 deletions deck.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package main

import (
"fmt"
"log"
)

type Deck struct {
Widgets []Widget
}

func LoadDeck(deck string) (*Deck, error) {
d := Deck{}
dc, err := LoadConfig(deck)
if err != nil {
return nil, err
}

for _, k := range dc.Keys {
w := NewWidget(k.Index, k.Widget.ID, k.Action, k.Widget.Config)
d.Widgets = append(d.Widgets, w)
}

return &d, nil
}

func (d *Deck) triggerAction(index uint8) {
for _, w := range d.Widgets {
if w.Key() == index {
a := w.Action()
if a != nil {
fmt.Println("Executing overwritten action")
if a.Deck != "" {
d, err := LoadDeck(a.Deck)
if err != nil {
log.Fatal(err)
}
err = dev.Clear()
if err != nil {
log.Fatal(err)
}

deck = d
deck.updateWidgets()
}
} else {
w.TriggerAction()
}
}
}
}

func (d *Deck) updateWidgets() {
for _, w := range d.Widgets {
w.Update(&dev)
}
}
64 changes: 30 additions & 34 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,37 +1,24 @@
package main

import (
"flag"
"fmt"
"image"
"log"
"os"
"time"

"image/draw"
_ "image/jpeg"
_ "image/png"

"github.com/davecgh/go-spew/spew"
"github.com/muesli/streamdeck"
)

var (
dev streamdeck.Device
deck *Deck
x Xorg
recentWindows []Window
)

func updateRecentApps(dev streamdeck.Device) {
for i := 0; i < int(dev.Columns)*int(dev.Rows); i++ {
img := image.NewRGBA(image.Rect(0, 0, 72, 72))

if i < len(recentWindows) {
draw.Draw(img, image.Rect(4, 4, 68, 68), recentWindows[i].Icon, image.Point{0, 0}, draw.Src)
}

err := dev.SetImage(uint8(i), img)
if err != nil {
panic(err)
}
}
}
deckFile = flag.String("deck", "deckmaster.deck", "path to deck config file")
)

func handleActiveWindowChanged(dev streamdeck.Device, event ActiveWindowChangedEvent) {
fmt.Println(fmt.Sprintf("Active window changed to %s (%d, %s)",
Expand All @@ -53,7 +40,7 @@ func handleActiveWindowChanged(dev streamdeck.Device, event ActiveWindowChangedE
if len(recentWindows) > 15 {
recentWindows = recentWindows[0:15]
}
updateRecentApps(dev)
deck.updateWidgets()
}

func handleWindowClosed(dev streamdeck.Device, event WindowClosedEvent) {
Expand All @@ -67,65 +54,74 @@ func handleWindowClosed(dev streamdeck.Device, event WindowClosedEvent) {
i++
}
recentWindows = recentWindows[:i]
updateRecentApps(dev)

deck.updateWidgets()
}

func main() {
x := Connect(os.Getenv("DISPLAY"))
flag.Parse()

var err error
deck, err = LoadDeck(*deckFile)
if err != nil {
log.Fatal(err)
}

x = Connect(os.Getenv("DISPLAY"))
defer x.Close()

tch := make(chan interface{})
x.TrackWindows(tch, time.Second)

d, err := streamdeck.Devices()
if err != nil {
panic(err)
log.Fatal(err)
}
if len(d) == 0 {
fmt.Println("No Stream Deck devices found.")
return
}
dev := d[0]
dev = d[0]

err = dev.Open()
if err != nil {
panic(err)
log.Fatal(err)
}
ver, err := dev.FirmwareVersion()
if err != nil {
panic(err)
log.Fatal(err)
}
fmt.Printf("Found device with serial %s (firmware %s)\n",
dev.Serial, ver)

err = dev.Reset()
if err != nil {
panic(err)
log.Fatal(err)
}
err = dev.SetBrightness(80)
if err != nil {
panic(err)
log.Fatal(err)
}

kch, err := dev.ReadKeys()
if err != nil {
panic(err)
log.Fatal(err)
}
for {
select {
case <-time.After(500 * time.Millisecond):
deck.updateWidgets()
case k, ok := <-kch:
if !ok {
err = dev.Open()
if err != nil {
panic(err)
log.Fatal(err)
}
continue
}
spew.Dump(k)

if k.Pressed && int(k.Index) < len(recentWindows) {
x.RequestActivation(recentWindows[k.Index])
if k.Pressed {
deck.triggerAction(k.Index)
}
case e := <-tch:
switch event := e.(type) {
Expand Down
66 changes: 66 additions & 0 deletions widget.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package main

import (
"io/ioutil"
"log"
"strconv"

"github.com/golang/freetype"
"github.com/golang/freetype/truetype"
"github.com/muesli/streamdeck"
)

var (
ttfFont *truetype.Font
)

type Widget interface {
Key() uint8
Update(dev *streamdeck.Device)
Action() *ActionConfig
TriggerAction()
}

type BaseWidget struct {
key uint8
action *ActionConfig
}

func (w *BaseWidget) Key() uint8 {
return w.key
}

func (w *BaseWidget) Action() *ActionConfig {
return w.action
}

func NewWidget(index uint8, id string, action *ActionConfig, config map[string]string) Widget {
bw := BaseWidget{index, action}

switch id {
case "recentWindow":
i, err := strconv.ParseUint(config["window"], 10, 64)
if err != nil {
log.Fatal(err)
}
return &RecentWindowWidget{BaseWidget: bw, window: uint8(i)}
case "top":
return &TopWidget{bw}
case "launcher":
return &LauncherWidget{BaseWidget: bw, launch: config["exec"], icon: config["icon"]}
}

return nil
}

func init() {
ttf, err := ioutil.ReadFile("/usr/share/fonts/TTF/Roboto-Medium.ttf")
if err != nil {
log.Fatal(err)
}

ttfFont, err = freetype.ParseFont(ttf)
if err != nil {
log.Fatal(err)
}
}
55 changes: 55 additions & 0 deletions widget_launcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package main

import (
"image"
"log"
"os"
"syscall"

"github.com/muesli/streamdeck"
"github.com/nfnt/resize"
)

type LauncherWidget struct {
BaseWidget
icon string
launch string
}

func (w *LauncherWidget) Update(dev *streamdeck.Device) {
f, err := os.Open(w.icon)
if err != nil {
log.Fatal(err)
}
img, _, err := image.Decode(f)
if err != nil {
log.Fatal(err)
}

err = dev.SetImage(w.key, resize.Resize(72, 72, img, resize.Lanczos3))
if err != nil {
log.Fatal(err)
}
}

func (w *LauncherWidget) TriggerAction() {
var sysproc = &syscall.SysProcAttr{Noctty: true}
var attr = os.ProcAttr{
Dir: ".",
Env: os.Environ(),
Files: []*os.File{
os.Stdin,
nil,
nil,
},
Sys: sysproc,
}
proc, err := os.StartProcess(w.launch, []string{w.launch}, &attr)
if err != nil {
log.Fatal(err)
}
err = proc.Release()
if err != nil {
log.Fatal(err)
}
}
Loading

0 comments on commit b304b47

Please sign in to comment.