Compare commits
4 Commits
cdbfec8bb7
...
f73fed6d84
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f73fed6d84 | ||
|
|
993756117c | ||
|
|
799ef45e1a | ||
|
|
feae481302 |
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024-2026 Wakatron
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
10
README.md
10
README.md
@ -398,6 +398,16 @@ Dependencies:
|
||||
|
||||
---
|
||||
|
||||
## Credits
|
||||
|
||||
This project builds on the work of the following open-source libraries:
|
||||
|
||||
- **[dh1tw/streamdeck](https://github.com/dh1tw/streamdeck)** (MIT) — Go protocol driver for Elgato Stream Deck devices
|
||||
- **[bearsh/hid](https://github.com/bearsh/hid)** (MIT) — Go USB HID bindings via CGO / libusb
|
||||
- **[disintegration/gift](https://github.com/disintegration/gift)** (MIT) — Go Image Filtering Toolkit
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
39
daemon.go
39
daemon.go
@ -15,6 +15,7 @@ type RunOptions struct {
|
||||
HTTPAddr string
|
||||
HTTPEnabled bool
|
||||
NoDeck bool
|
||||
StartPage string
|
||||
}
|
||||
|
||||
func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
|
||||
@ -63,6 +64,16 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
|
||||
primaryPM := pageMgrs[0]
|
||||
primaryDeck := decks[0]
|
||||
|
||||
startPage := opts.StartPage
|
||||
if startPage == "" {
|
||||
startPage = cfg.DefaultPage
|
||||
}
|
||||
if !pageExists(cfg.Pages, startPage) {
|
||||
slog.Warn("start page not found in pages, falling back to default",
|
||||
"requested", startPage, "default", cfg.DefaultPage)
|
||||
startPage = cfg.DefaultPage
|
||||
}
|
||||
|
||||
defer func() {
|
||||
for _, d := range decks {
|
||||
d.Close()
|
||||
@ -78,8 +89,8 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
|
||||
web.SetExtraPageManagers(pageMgrs[1:])
|
||||
|
||||
for _, pm := range pageMgrs {
|
||||
if err := pm.ActivatePage(cfg.DefaultPage); err != nil {
|
||||
slog.Warn("activate default page", "error", err)
|
||||
if err := pm.ActivatePage(startPage); err != nil {
|
||||
slog.Warn("activate start page", "error", err)
|
||||
}
|
||||
pm.startPeriodicKeys()
|
||||
}
|
||||
@ -98,7 +109,7 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
|
||||
windowCh, _ = detector.Start(ctx)
|
||||
}
|
||||
|
||||
asm := NewAutoSwitchManager(cfg.AutoSwitch, cfg.DefaultPage)
|
||||
asm := NewAutoSwitchManager(cfg.AutoSwitch, startPage)
|
||||
|
||||
ssCtrl := NewScreensaver(&cfg.Screensaver)
|
||||
|
||||
@ -149,7 +160,7 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
|
||||
asm.Pause()
|
||||
|
||||
primaryDeck.Close()
|
||||
newDeck := reconnectDeck(ctx, cfg, &primaryPM)
|
||||
newDeck := reconnectDeck(ctx, cfg, &primaryPM, startPage)
|
||||
if newDeck == nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
@ -162,7 +173,7 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
|
||||
web.SetPageManager(primaryPM)
|
||||
|
||||
newDeck.SetBrightness(deviceBrightness(cfg, newDeck.Serial()))
|
||||
asm.NotifyManualPage(cfg.DefaultPage)
|
||||
asm.NotifyManualPage(startPage)
|
||||
asm.Resume()
|
||||
continue
|
||||
}
|
||||
@ -288,7 +299,7 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
|
||||
asm.Pause()
|
||||
|
||||
primaryDeck.Close()
|
||||
newDeck := reconnectDeck(ctx, cfg, &primaryPM)
|
||||
newDeck := reconnectDeck(ctx, cfg, &primaryPM, startPage)
|
||||
if newDeck == nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
@ -301,7 +312,7 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
|
||||
web.SetPageManager(primaryPM)
|
||||
|
||||
newDeck.SetBrightness(deviceBrightness(cfg, newDeck.Serial()))
|
||||
asm.NotifyManualPage(cfg.DefaultPage)
|
||||
asm.NotifyManualPage(startPage)
|
||||
asm.Resume()
|
||||
}
|
||||
|
||||
@ -353,7 +364,7 @@ func deviceBrightness(cfg *config.Config, serial string) int {
|
||||
return 75
|
||||
}
|
||||
|
||||
func reconnectDeck(ctx context.Context, cfg *config.Config, pm **PageManager) *Deck {
|
||||
func reconnectDeck(ctx context.Context, cfg *config.Config, pm **PageManager, startPage string) *Deck {
|
||||
for {
|
||||
newDeck, err := OpenDeck("")
|
||||
if err == nil {
|
||||
@ -361,7 +372,7 @@ func reconnectDeck(ctx context.Context, cfg *config.Config, pm **PageManager) *D
|
||||
(*pm).defaultFont = cfg.Font
|
||||
(*pm).showLabelBackground = cfg.ShowLabelBackground
|
||||
(*pm).LoadPages(cfg.Pages)
|
||||
(*pm).ActivatePage(cfg.DefaultPage)
|
||||
(*pm).ActivatePage(startPage)
|
||||
(*pm).startPeriodicKeys()
|
||||
return newDeck
|
||||
}
|
||||
@ -372,3 +383,13 @@ func reconnectDeck(ctx context.Context, cfg *config.Config, pm **PageManager) *D
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pageExists reports whether pages contains a page with the given name.
|
||||
func pageExists(pages []config.PageConfig, name string) bool {
|
||||
for _, p := range pages {
|
||||
if p.Name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
23
dist/arch/.SRCINFO
vendored
Normal file
23
dist/arch/.SRCINFO
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
pkgbase = streamdeck-lets-go-git
|
||||
pkgdesc = Lightweight daemon for controlling Elgato Stream Deck devices with a built-in web UI
|
||||
pkgver = r0.unknown
|
||||
pkgrel = 1
|
||||
url = https://github.com/Wakatron/streamdeck-lets-go
|
||||
arch = x86_64
|
||||
arch = aarch64
|
||||
license = MIT
|
||||
makedepends = go>=1.26
|
||||
makedepends = git
|
||||
depends = libusb-1.0
|
||||
optdepends = librsvg: SVG icon rendering
|
||||
optdepends = fontconfig: system font detection
|
||||
provides = streamdeck-lets-go
|
||||
conflicts = streamdeck-lets-go
|
||||
source = streamdeck-lets-go-git::git+https://github.com/Wakatron/streamdeck-lets-go.git
|
||||
source = streamdeck-lets-go.service
|
||||
source = 90-streamdeck.rules
|
||||
b2sums = SKIP
|
||||
b2sums = SKIP
|
||||
b2sums = SKIP
|
||||
|
||||
pkgname = streamdeck-lets-go-git
|
||||
7
dist/arch/90-streamdeck.rules
vendored
Normal file
7
dist/arch/90-streamdeck.rules
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
# Elgato Stream Deck — allow access for locally logged-in users
|
||||
# Vendor 0fd9
|
||||
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0060", TAG+="uaccess"
|
||||
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0063", TAG+="uaccess"
|
||||
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="006c", TAG+="uaccess"
|
||||
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="006d", TAG+="uaccess"
|
||||
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0080", TAG+="uaccess"
|
||||
48
dist/arch/PKGBUILD
vendored
Normal file
48
dist/arch/PKGBUILD
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
# Maintainer: Wakatron <wakatron@example.com>
|
||||
|
||||
pkgname=streamdeck-lets-go-git
|
||||
_giturl=https://github.com/Wakatron/streamdeck-lets-go.git
|
||||
pkgver=r0.unknown
|
||||
pkgrel=1
|
||||
pkgdesc="Lightweight daemon for controlling Elgato Stream Deck devices with a built-in web UI"
|
||||
arch=('x86_64' 'aarch64')
|
||||
url="https://github.com/Wakatron/streamdeck-lets-go"
|
||||
license=('MIT')
|
||||
depends=('libusb-1.0')
|
||||
optdepends=('librsvg: SVG icon rendering'
|
||||
'fontconfig: system font detection')
|
||||
makedepends=('go>=1.26' 'git')
|
||||
provides=('streamdeck-lets-go')
|
||||
conflicts=('streamdeck-lets-go')
|
||||
|
||||
source=("${pkgname}::git+${_giturl}"
|
||||
"streamdeck-lets-go.service"
|
||||
"90-streamdeck.rules")
|
||||
b2sums=('SKIP'
|
||||
'SKIP'
|
||||
'SKIP')
|
||||
|
||||
pkgver() {
|
||||
cd "${srcdir}/${pkgname}"
|
||||
git describe --always --tags 2>/dev/null || echo "r0.$(git rev-parse --short HEAD)"
|
||||
}
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}"
|
||||
export CGO_ENABLED=1
|
||||
go build \
|
||||
-ldflags "-s -w" \
|
||||
-o streamdeck-lets-go .
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "${srcdir}/${pkgname}"
|
||||
|
||||
install -Dm755 streamdeck-lets-go "${pkgdir}/usr/bin/streamdeck-lets-go"
|
||||
|
||||
install -Dm644 "${srcdir}/streamdeck-lets-go.service" \
|
||||
"${pkgdir}/usr/lib/systemd/user/streamdeck-lets-go.service"
|
||||
|
||||
install -Dm644 "${srcdir}/90-streamdeck.rules" \
|
||||
"${pkgdir}/usr/lib/udev/rules.d/90-streamdeck.rules"
|
||||
}
|
||||
14
dist/arch/streamdeck-lets-go.service
vendored
Normal file
14
dist/arch/streamdeck-lets-go.service
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=Stream Deck Lets Go daemon
|
||||
Documentation=https://github.com/Wakatron/streamdeck-lets-go
|
||||
After=graphical-session.target
|
||||
Wants=graphical-session.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/streamdeck-lets-go daemon
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
53
main.go
53
main.go
@ -1,12 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
_ "image/gif"
|
||||
@ -22,15 +26,16 @@ func main() {
|
||||
fs := flag.NewFlagSet("streamdeck-lets-go", flag.ExitOnError)
|
||||
configPath := fs.String("config", "", "path to config.json (default: ~/.config/streamdeck-lets-go/config.json)")
|
||||
httpAddr := fs.String("addr", ":9090", "web UI listen address")
|
||||
startPage := fs.String("page", "", "page to activate on startup (default: config's default_page)")
|
||||
noDeck := fs.Bool("no-deck", false, "run without Stream Deck hardware (config editing only)")
|
||||
verbose := fs.Bool("v", false, "verbose output")
|
||||
|
||||
if len(os.Args) < 2 {
|
||||
if len(os.Args) < 2 || os.Args[1] == "--help" || os.Args[1] == "-help" || os.Args[1] == "-h" {
|
||||
fmt.Fprintf(os.Stderr, "Usage: streamdeck-lets-go [flags] [command]\n\n")
|
||||
fmt.Fprintf(os.Stderr, "Commands:\n")
|
||||
fmt.Fprintf(os.Stderr, " daemon run the Stream Deck daemon with web UI (default)\n")
|
||||
fmt.Fprintf(os.Stderr, " serve run web UI only (no deck hardware)\n")
|
||||
fmt.Fprintf(os.Stderr, " discover list connected Stream Decks\n")
|
||||
fmt.Fprintf(os.Stderr, " daemon run the Stream Deck daemon with web UI (default)\n")
|
||||
fmt.Fprintf(os.Stderr, " switch switch running daemon to a page (e.g. 'switch games')\n")
|
||||
fmt.Fprintf(os.Stderr, " discover list connected Stream Decks\n")
|
||||
fmt.Fprintf(os.Stderr, "\nFlags:\n")
|
||||
fs.PrintDefaults()
|
||||
os.Exit(0)
|
||||
@ -42,13 +47,26 @@ func main() {
|
||||
switch cmd {
|
||||
case "daemon":
|
||||
fs.Parse(args)
|
||||
case "serve":
|
||||
case "switch":
|
||||
fs.Parse(args)
|
||||
*noDeck = true
|
||||
if fs.NArg() == 0 {
|
||||
fmt.Fprintf(os.Stderr, "Usage: streamdeck-lets-go switch <page>\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := switchToPage(*httpAddr, fs.Arg(0)); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("switched to page %q\n", fs.Arg(0))
|
||||
return
|
||||
case "discover":
|
||||
discover()
|
||||
return
|
||||
default:
|
||||
if cmd == "serve" {
|
||||
fmt.Fprintf(os.Stderr, "streamdeck-lets-go: 'serve' removed; use 'daemon --no-deck' instead\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
fs.Parse(os.Args[1:])
|
||||
}
|
||||
|
||||
@ -71,6 +89,7 @@ func main() {
|
||||
HTTPAddr: *httpAddr,
|
||||
HTTPEnabled: httpEnabled,
|
||||
NoDeck: *noDeck,
|
||||
StartPage: *startPage,
|
||||
}); err != nil {
|
||||
slog.Error("run", "error", err)
|
||||
os.Exit(1)
|
||||
@ -92,3 +111,25 @@ func discover() {
|
||||
fmt.Printf(" Serial: %s Model: %s PID: 0x%04x\n", d.Serial, d.Model, d.PID)
|
||||
}
|
||||
}
|
||||
|
||||
// switchToPage tells the running daemon (at httpAddr) to activate the given page
|
||||
// by POSTing to its /api/activate-page endpoint.
|
||||
func switchToPage(httpAddr, page string) error {
|
||||
host := httpAddr
|
||||
if strings.HasPrefix(host, ":") {
|
||||
host = "127.0.0.1" + host
|
||||
}
|
||||
url := "http://" + host + "/api/activate-page"
|
||||
|
||||
body, _ := json.Marshal(map[string]string{"page": page})
|
||||
resp, err := http.Post(url, "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("daemon not running? %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("switch failed (HTTP %d)", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user