Compare commits
No commits in common. "f73fed6d84f9e0c676021bad139673593c79e340" and "cdbfec8bb7918958f876e426e9edd8d7e733e855" have entirely different histories.
f73fed6d84
...
cdbfec8bb7
21
LICENSE
21
LICENSE
@ -1,21 +0,0 @@
|
|||||||
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,16 +398,6 @@ 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
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
39
daemon.go
39
daemon.go
@ -15,7 +15,6 @@ type RunOptions struct {
|
|||||||
HTTPAddr string
|
HTTPAddr string
|
||||||
HTTPEnabled bool
|
HTTPEnabled bool
|
||||||
NoDeck bool
|
NoDeck bool
|
||||||
StartPage string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
|
func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
|
||||||
@ -64,16 +63,6 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
|
|||||||
primaryPM := pageMgrs[0]
|
primaryPM := pageMgrs[0]
|
||||||
primaryDeck := decks[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() {
|
defer func() {
|
||||||
for _, d := range decks {
|
for _, d := range decks {
|
||||||
d.Close()
|
d.Close()
|
||||||
@ -89,8 +78,8 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
|
|||||||
web.SetExtraPageManagers(pageMgrs[1:])
|
web.SetExtraPageManagers(pageMgrs[1:])
|
||||||
|
|
||||||
for _, pm := range pageMgrs {
|
for _, pm := range pageMgrs {
|
||||||
if err := pm.ActivatePage(startPage); err != nil {
|
if err := pm.ActivatePage(cfg.DefaultPage); err != nil {
|
||||||
slog.Warn("activate start page", "error", err)
|
slog.Warn("activate default page", "error", err)
|
||||||
}
|
}
|
||||||
pm.startPeriodicKeys()
|
pm.startPeriodicKeys()
|
||||||
}
|
}
|
||||||
@ -109,7 +98,7 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
|
|||||||
windowCh, _ = detector.Start(ctx)
|
windowCh, _ = detector.Start(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
asm := NewAutoSwitchManager(cfg.AutoSwitch, startPage)
|
asm := NewAutoSwitchManager(cfg.AutoSwitch, cfg.DefaultPage)
|
||||||
|
|
||||||
ssCtrl := NewScreensaver(&cfg.Screensaver)
|
ssCtrl := NewScreensaver(&cfg.Screensaver)
|
||||||
|
|
||||||
@ -160,7 +149,7 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
|
|||||||
asm.Pause()
|
asm.Pause()
|
||||||
|
|
||||||
primaryDeck.Close()
|
primaryDeck.Close()
|
||||||
newDeck := reconnectDeck(ctx, cfg, &primaryPM, startPage)
|
newDeck := reconnectDeck(ctx, cfg, &primaryPM)
|
||||||
if newDeck == nil {
|
if newDeck == nil {
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
@ -173,7 +162,7 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
|
|||||||
web.SetPageManager(primaryPM)
|
web.SetPageManager(primaryPM)
|
||||||
|
|
||||||
newDeck.SetBrightness(deviceBrightness(cfg, newDeck.Serial()))
|
newDeck.SetBrightness(deviceBrightness(cfg, newDeck.Serial()))
|
||||||
asm.NotifyManualPage(startPage)
|
asm.NotifyManualPage(cfg.DefaultPage)
|
||||||
asm.Resume()
|
asm.Resume()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -299,7 +288,7 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
|
|||||||
asm.Pause()
|
asm.Pause()
|
||||||
|
|
||||||
primaryDeck.Close()
|
primaryDeck.Close()
|
||||||
newDeck := reconnectDeck(ctx, cfg, &primaryPM, startPage)
|
newDeck := reconnectDeck(ctx, cfg, &primaryPM)
|
||||||
if newDeck == nil {
|
if newDeck == nil {
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
@ -312,7 +301,7 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
|
|||||||
web.SetPageManager(primaryPM)
|
web.SetPageManager(primaryPM)
|
||||||
|
|
||||||
newDeck.SetBrightness(deviceBrightness(cfg, newDeck.Serial()))
|
newDeck.SetBrightness(deviceBrightness(cfg, newDeck.Serial()))
|
||||||
asm.NotifyManualPage(startPage)
|
asm.NotifyManualPage(cfg.DefaultPage)
|
||||||
asm.Resume()
|
asm.Resume()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,7 +353,7 @@ func deviceBrightness(cfg *config.Config, serial string) int {
|
|||||||
return 75
|
return 75
|
||||||
}
|
}
|
||||||
|
|
||||||
func reconnectDeck(ctx context.Context, cfg *config.Config, pm **PageManager, startPage string) *Deck {
|
func reconnectDeck(ctx context.Context, cfg *config.Config, pm **PageManager) *Deck {
|
||||||
for {
|
for {
|
||||||
newDeck, err := OpenDeck("")
|
newDeck, err := OpenDeck("")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -372,7 +361,7 @@ func reconnectDeck(ctx context.Context, cfg *config.Config, pm **PageManager, st
|
|||||||
(*pm).defaultFont = cfg.Font
|
(*pm).defaultFont = cfg.Font
|
||||||
(*pm).showLabelBackground = cfg.ShowLabelBackground
|
(*pm).showLabelBackground = cfg.ShowLabelBackground
|
||||||
(*pm).LoadPages(cfg.Pages)
|
(*pm).LoadPages(cfg.Pages)
|
||||||
(*pm).ActivatePage(startPage)
|
(*pm).ActivatePage(cfg.DefaultPage)
|
||||||
(*pm).startPeriodicKeys()
|
(*pm).startPeriodicKeys()
|
||||||
return newDeck
|
return newDeck
|
||||||
}
|
}
|
||||||
@ -383,13 +372,3 @@ func reconnectDeck(ctx context.Context, cfg *config.Config, pm **PageManager, st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
23
dist/arch/.SRCINFO
vendored
@ -1,23 +0,0 @@
|
|||||||
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
7
dist/arch/90-streamdeck.rules
vendored
@ -1,7 +0,0 @@
|
|||||||
# 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
48
dist/arch/PKGBUILD
vendored
@ -1,48 +0,0 @@
|
|||||||
# 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
14
dist/arch/streamdeck-lets-go.service
vendored
@ -1,14 +0,0 @@
|
|||||||
[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
|
|
||||||
49
main.go
49
main.go
@ -1,16 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
_ "image/gif"
|
_ "image/gif"
|
||||||
@ -26,15 +22,14 @@ func main() {
|
|||||||
fs := flag.NewFlagSet("streamdeck-lets-go", flag.ExitOnError)
|
fs := flag.NewFlagSet("streamdeck-lets-go", flag.ExitOnError)
|
||||||
configPath := fs.String("config", "", "path to config.json (default: ~/.config/streamdeck-lets-go/config.json)")
|
configPath := fs.String("config", "", "path to config.json (default: ~/.config/streamdeck-lets-go/config.json)")
|
||||||
httpAddr := fs.String("addr", ":9090", "web UI listen address")
|
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)")
|
noDeck := fs.Bool("no-deck", false, "run without Stream Deck hardware (config editing only)")
|
||||||
verbose := fs.Bool("v", false, "verbose output")
|
verbose := fs.Bool("v", false, "verbose output")
|
||||||
|
|
||||||
if len(os.Args) < 2 || os.Args[1] == "--help" || os.Args[1] == "-help" || os.Args[1] == "-h" {
|
if len(os.Args) < 2 {
|
||||||
fmt.Fprintf(os.Stderr, "Usage: streamdeck-lets-go [flags] [command]\n\n")
|
fmt.Fprintf(os.Stderr, "Usage: streamdeck-lets-go [flags] [command]\n\n")
|
||||||
fmt.Fprintf(os.Stderr, "Commands:\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, " 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, " serve run web UI only (no deck hardware)\n")
|
||||||
fmt.Fprintf(os.Stderr, " discover list connected Stream Decks\n")
|
fmt.Fprintf(os.Stderr, " discover list connected Stream Decks\n")
|
||||||
fmt.Fprintf(os.Stderr, "\nFlags:\n")
|
fmt.Fprintf(os.Stderr, "\nFlags:\n")
|
||||||
fs.PrintDefaults()
|
fs.PrintDefaults()
|
||||||
@ -47,26 +42,13 @@ func main() {
|
|||||||
switch cmd {
|
switch cmd {
|
||||||
case "daemon":
|
case "daemon":
|
||||||
fs.Parse(args)
|
fs.Parse(args)
|
||||||
case "switch":
|
case "serve":
|
||||||
fs.Parse(args)
|
fs.Parse(args)
|
||||||
if fs.NArg() == 0 {
|
*noDeck = true
|
||||||
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":
|
case "discover":
|
||||||
discover()
|
discover()
|
||||||
return
|
return
|
||||||
default:
|
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:])
|
fs.Parse(os.Args[1:])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +71,6 @@ func main() {
|
|||||||
HTTPAddr: *httpAddr,
|
HTTPAddr: *httpAddr,
|
||||||
HTTPEnabled: httpEnabled,
|
HTTPEnabled: httpEnabled,
|
||||||
NoDeck: *noDeck,
|
NoDeck: *noDeck,
|
||||||
StartPage: *startPage,
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
slog.Error("run", "error", err)
|
slog.Error("run", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@ -111,25 +92,3 @@ func discover() {
|
|||||||
fmt.Printf(" Serial: %s Model: %s PID: 0x%04x\n", d.Serial, d.Model, d.PID)
|
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