Skip to content

Commit b304b47

Browse files
committed
Added support for loading deck definitions, launching apps and displaying CPU usage
1 parent cd87951 commit b304b47

7 files changed

Lines changed: 377 additions & 34 deletions

File tree

config.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"io/ioutil"
6+
7+
"github.com/BurntSushi/toml"
8+
)
9+
10+
type DBusConfig struct {
11+
Object string `toml:"object,omitempty"`
12+
Path string `toml:"path,omitempty"`
13+
Method string `toml:"method,omitempty"`
14+
Value string `toml:"value,omitempty"`
15+
}
16+
17+
type ActionConfig struct {
18+
Keycode string `toml:"keycode,omitempty"`
19+
Exec string `toml:"exec,omitempty"`
20+
Deck string `toml:"deck,omitempty"`
21+
DBus DBusConfig `toml:"dbus,omitempty"`
22+
}
23+
24+
type WidgetConfig struct {
25+
ID string `toml:"id,omitempty"`
26+
Config map[string]string `toml:"config,omitempty"`
27+
}
28+
29+
type KeyConfig struct {
30+
Index uint8 `toml:"index"`
31+
Widget WidgetConfig `toml:"widget"`
32+
Action *ActionConfig `toml:"action,omitempty"`
33+
}
34+
type Keys []KeyConfig
35+
36+
type DeckConfig struct {
37+
Keys Keys `toml:"keys"`
38+
}
39+
40+
// LoadConfig loads config from filename
41+
func LoadConfig(filename string) (DeckConfig, error) {
42+
config := DeckConfig{}
43+
44+
b, err := ioutil.ReadFile(filename)
45+
if err != nil {
46+
return config, err
47+
}
48+
49+
_, err = toml.Decode(string(b), &config)
50+
return config, err
51+
}
52+
53+
// Save writes config as json to filename
54+
func (c DeckConfig) Save(filename string) error {
55+
var b bytes.Buffer
56+
e := toml.NewEncoder(&b)
57+
err := e.Encode(c)
58+
if err != nil {
59+
return err
60+
}
61+
62+
return ioutil.WriteFile(filename, b.Bytes(), 0644)
63+
}

deck.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
)
7+
8+
type Deck struct {
9+
Widgets []Widget
10+
}
11+
12+
func LoadDeck(deck string) (*Deck, error) {
13+
d := Deck{}
14+
dc, err := LoadConfig(deck)
15+
if err != nil {
16+
return nil, err
17+
}
18+
19+
for _, k := range dc.Keys {
20+
w := NewWidget(k.Index, k.Widget.ID, k.Action, k.Widget.Config)
21+
d.Widgets = append(d.Widgets, w)
22+
}
23+
24+
return &d, nil
25+
}
26+
27+
func (d *Deck) triggerAction(index uint8) {
28+
for _, w := range d.Widgets {
29+
if w.Key() == index {
30+
a := w.Action()
31+
if a != nil {
32+
fmt.Println("Executing overwritten action")
33+
if a.Deck != "" {
34+
d, err := LoadDeck(a.Deck)
35+
if err != nil {
36+
log.Fatal(err)
37+
}
38+
err = dev.Clear()
39+
if err != nil {
40+
log.Fatal(err)
41+
}
42+
43+
deck = d
44+
deck.updateWidgets()
45+
}
46+
} else {
47+
w.TriggerAction()
48+
}
49+
}
50+
}
51+
}
52+
53+
func (d *Deck) updateWidgets() {
54+
for _, w := range d.Widgets {
55+
w.Update(&dev)
56+
}
57+
}

main.go

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,24 @@
11
package main
22

33
import (
4+
"flag"
45
"fmt"
5-
"image"
6+
"log"
67
"os"
78
"time"
89

9-
"image/draw"
10-
_ "image/jpeg"
11-
_ "image/png"
12-
1310
"github.com/davecgh/go-spew/spew"
1411
"github.com/muesli/streamdeck"
1512
)
1613

1714
var (
15+
dev streamdeck.Device
16+
deck *Deck
17+
x Xorg
1818
recentWindows []Window
19-
)
2019

21-
func updateRecentApps(dev streamdeck.Device) {
22-
for i := 0; i < int(dev.Columns)*int(dev.Rows); i++ {
23-
img := image.NewRGBA(image.Rect(0, 0, 72, 72))
24-
25-
if i < len(recentWindows) {
26-
draw.Draw(img, image.Rect(4, 4, 68, 68), recentWindows[i].Icon, image.Point{0, 0}, draw.Src)
27-
}
28-
29-
err := dev.SetImage(uint8(i), img)
30-
if err != nil {
31-
panic(err)
32-
}
33-
}
34-
}
20+
deckFile = flag.String("deck", "deckmaster.deck", "path to deck config file")
21+
)
3522

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

5946
func handleWindowClosed(dev streamdeck.Device, event WindowClosedEvent) {
@@ -67,65 +54,74 @@ func handleWindowClosed(dev streamdeck.Device, event WindowClosedEvent) {
6754
i++
6855
}
6956
recentWindows = recentWindows[:i]
70-
updateRecentApps(dev)
71-
57+
deck.updateWidgets()
7258
}
7359

7460
func main() {
75-
x := Connect(os.Getenv("DISPLAY"))
61+
flag.Parse()
62+
63+
var err error
64+
deck, err = LoadDeck(*deckFile)
65+
if err != nil {
66+
log.Fatal(err)
67+
}
68+
69+
x = Connect(os.Getenv("DISPLAY"))
7670
defer x.Close()
7771

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

8175
d, err := streamdeck.Devices()
8276
if err != nil {
83-
panic(err)
77+
log.Fatal(err)
8478
}
8579
if len(d) == 0 {
8680
fmt.Println("No Stream Deck devices found.")
8781
return
8882
}
89-
dev := d[0]
83+
dev = d[0]
9084

9185
err = dev.Open()
9286
if err != nil {
93-
panic(err)
87+
log.Fatal(err)
9488
}
9589
ver, err := dev.FirmwareVersion()
9690
if err != nil {
97-
panic(err)
91+
log.Fatal(err)
9892
}
9993
fmt.Printf("Found device with serial %s (firmware %s)\n",
10094
dev.Serial, ver)
10195

10296
err = dev.Reset()
10397
if err != nil {
104-
panic(err)
98+
log.Fatal(err)
10599
}
106100
err = dev.SetBrightness(80)
107101
if err != nil {
108-
panic(err)
102+
log.Fatal(err)
109103
}
110104

111105
kch, err := dev.ReadKeys()
112106
if err != nil {
113-
panic(err)
107+
log.Fatal(err)
114108
}
115109
for {
116110
select {
111+
case <-time.After(500 * time.Millisecond):
112+
deck.updateWidgets()
117113
case k, ok := <-kch:
118114
if !ok {
119115
err = dev.Open()
120116
if err != nil {
121-
panic(err)
117+
log.Fatal(err)
122118
}
123119
continue
124120
}
125121
spew.Dump(k)
126122

127-
if k.Pressed && int(k.Index) < len(recentWindows) {
128-
x.RequestActivation(recentWindows[k.Index])
123+
if k.Pressed {
124+
deck.triggerAction(k.Index)
129125
}
130126
case e := <-tch:
131127
switch event := e.(type) {

widget.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package main
2+
3+
import (
4+
"io/ioutil"
5+
"log"
6+
"strconv"
7+
8+
"github.com/golang/freetype"
9+
"github.com/golang/freetype/truetype"
10+
"github.com/muesli/streamdeck"
11+
)
12+
13+
var (
14+
ttfFont *truetype.Font
15+
)
16+
17+
type Widget interface {
18+
Key() uint8
19+
Update(dev *streamdeck.Device)
20+
Action() *ActionConfig
21+
TriggerAction()
22+
}
23+
24+
type BaseWidget struct {
25+
key uint8
26+
action *ActionConfig
27+
}
28+
29+
func (w *BaseWidget) Key() uint8 {
30+
return w.key
31+
}
32+
33+
func (w *BaseWidget) Action() *ActionConfig {
34+
return w.action
35+
}
36+
37+
func NewWidget(index uint8, id string, action *ActionConfig, config map[string]string) Widget {
38+
bw := BaseWidget{index, action}
39+
40+
switch id {
41+
case "recentWindow":
42+
i, err := strconv.ParseUint(config["window"], 10, 64)
43+
if err != nil {
44+
log.Fatal(err)
45+
}
46+
return &RecentWindowWidget{BaseWidget: bw, window: uint8(i)}
47+
case "top":
48+
return &TopWidget{bw}
49+
case "launcher":
50+
return &LauncherWidget{BaseWidget: bw, launch: config["exec"], icon: config["icon"]}
51+
}
52+
53+
return nil
54+
}
55+
56+
func init() {
57+
ttf, err := ioutil.ReadFile("/usr/share/fonts/TTF/Roboto-Medium.ttf")
58+
if err != nil {
59+
log.Fatal(err)
60+
}
61+
62+
ttfFont, err = freetype.ParseFont(ttf)
63+
if err != nil {
64+
log.Fatal(err)
65+
}
66+
}

widget_launcher.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package main
2+
3+
import (
4+
"image"
5+
"log"
6+
"os"
7+
"syscall"
8+
9+
"github.com/muesli/streamdeck"
10+
"github.com/nfnt/resize"
11+
)
12+
13+
type LauncherWidget struct {
14+
BaseWidget
15+
icon string
16+
launch string
17+
}
18+
19+
func (w *LauncherWidget) Update(dev *streamdeck.Device) {
20+
f, err := os.Open(w.icon)
21+
if err != nil {
22+
log.Fatal(err)
23+
}
24+
img, _, err := image.Decode(f)
25+
if err != nil {
26+
log.Fatal(err)
27+
}
28+
29+
err = dev.SetImage(w.key, resize.Resize(72, 72, img, resize.Lanczos3))
30+
if err != nil {
31+
log.Fatal(err)
32+
}
33+
}
34+
35+
func (w *LauncherWidget) TriggerAction() {
36+
var sysproc = &syscall.SysProcAttr{Noctty: true}
37+
var attr = os.ProcAttr{
38+
Dir: ".",
39+
Env: os.Environ(),
40+
Files: []*os.File{
41+
os.Stdin,
42+
nil,
43+
nil,
44+
},
45+
Sys: sysproc,
46+
}
47+
proc, err := os.StartProcess(w.launch, []string{w.launch}, &attr)
48+
if err != nil {
49+
log.Fatal(err)
50+
}
51+
err = proc.Release()
52+
if err != nil {
53+
log.Fatal(err)
54+
}
55+
}

0 commit comments

Comments
 (0)