refactor: restructure to standard Go project layout (cmd/ + internal/)
- Move entry point to cmd/streamdeck-lets-go/ - Split package main into internal packages: - internal/deck: hardware control (Deck, events, brightness) - internal/render: key rendering, icons, fonts, PageManager - internal/web: HTTP API + embedded SPA - internal/daemon: event loop, autoswitch, screensaver, actions - Add ConfigDir() to internal/config - Export cross-package API (Deck, PageManager, WebServer, etc.) - Move dist/arch/ → packaging/ with updated PKGBUILD/.SRCINFO - Add Makefile (build, test, vet, fmt, install) - Update README with new build commands and project structure - Delete unused media.go stub
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"streamdeck-lets-go/internal/config"
|
||||
"streamdeck-lets-go/internal/deck"
|
||||
"streamdeck-lets-go/internal/render"
|
||||
)
|
||||
|
||||
func ExecuteAction(a *config.Action, deck *deck.Deck, pm *render.PageManager) error {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
slog.Debug("execute action", "type", a.Type)
|
||||
|
||||
switch a.Type {
|
||||
case "command":
|
||||
return execCommand(a)
|
||||
case "builtin":
|
||||
return execBuiltin(a, deck, pm)
|
||||
case "script":
|
||||
return execScript(a)
|
||||
case "page":
|
||||
if pm != nil {
|
||||
return pm.ActivatePage(a.Page)
|
||||
}
|
||||
return fmt.Errorf("page manager not available")
|
||||
case "keyboard":
|
||||
return execKeyboard(a)
|
||||
default:
|
||||
return fmt.Errorf("unknown action type: %s", a.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func execCommand(a *config.Action) error {
|
||||
cmd := exec.Command("sh", "-c", a.Command)
|
||||
|
||||
if a.Background {
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("start command: %w", err)
|
||||
}
|
||||
go func() {
|
||||
if err := cmd.Wait(); err != nil {
|
||||
slog.Warn("command finished", "cmd", a.Command, "error", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("run command: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func execScript(a *config.Action) error {
|
||||
cmd := exec.Command(a.Script)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("run script: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func execBuiltin(a *config.Action, deck *deck.Deck, pm *render.PageManager) error {
|
||||
parts := strings.SplitN(a.Builtin, ":", 2)
|
||||
if len(parts) < 1 {
|
||||
return fmt.Errorf("invalid builtin: %s", a.Builtin)
|
||||
}
|
||||
|
||||
category := parts[0]
|
||||
action := ""
|
||||
if len(parts) > 1 {
|
||||
action = parts[1]
|
||||
}
|
||||
|
||||
slog.Debug("builtin", "category", category, "action", action)
|
||||
|
||||
switch category {
|
||||
case "media":
|
||||
return mediaAction(action)
|
||||
case "volume":
|
||||
return volumeAction(action)
|
||||
case "brightness":
|
||||
return brightnessAction(action)
|
||||
case "page":
|
||||
if pm != nil {
|
||||
return pageBuiltinAction(action, pm)
|
||||
}
|
||||
return fmt.Errorf("page manager not available")
|
||||
case "deck":
|
||||
return deckBuiltinAction(action, deck)
|
||||
default:
|
||||
return fmt.Errorf("unknown builtin category: %s", category)
|
||||
}
|
||||
}
|
||||
|
||||
func mediaAction(action string) error {
|
||||
var cmd string
|
||||
switch action {
|
||||
case "playpause":
|
||||
cmd = "playerctl play-pause"
|
||||
case "next":
|
||||
cmd = "playerctl next"
|
||||
case "prev":
|
||||
cmd = "playerctl previous"
|
||||
case "stop":
|
||||
cmd = "playerctl stop"
|
||||
default:
|
||||
return fmt.Errorf("unknown media action: %s", action)
|
||||
}
|
||||
return exec.Command("sh", "-c", cmd).Run()
|
||||
}
|
||||
|
||||
func volumeAction(action string) error {
|
||||
var cmd string
|
||||
switch action {
|
||||
case "up":
|
||||
cmd = "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+"
|
||||
case "down":
|
||||
cmd = "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"
|
||||
case "mute":
|
||||
cmd = "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"
|
||||
default:
|
||||
return fmt.Errorf("unknown volume action: %s", action)
|
||||
}
|
||||
return exec.Command("sh", "-c", cmd).Run()
|
||||
}
|
||||
|
||||
func brightnessAction(action string) error {
|
||||
var cmd string
|
||||
switch action {
|
||||
case "up":
|
||||
cmd = "brightnessctl s +10%"
|
||||
case "down":
|
||||
cmd = "brightnessctl s 10%-"
|
||||
default:
|
||||
return fmt.Errorf("unknown brightness action: %s", action)
|
||||
}
|
||||
return exec.Command("sh", "-c", cmd).Run()
|
||||
}
|
||||
|
||||
func pageBuiltinAction(action string, pm *render.PageManager) error {
|
||||
switch action {
|
||||
case "next":
|
||||
pageNames := pm.GetPageNames()
|
||||
current := pm.ActivePageName()
|
||||
for i, name := range pageNames {
|
||||
if name == current {
|
||||
next := (i + 1) % len(pageNames)
|
||||
return pm.ActivatePage(pageNames[next])
|
||||
}
|
||||
}
|
||||
if len(pageNames) > 0 {
|
||||
return pm.ActivatePage(pageNames[0])
|
||||
}
|
||||
case "prev":
|
||||
pageNames := pm.GetPageNames()
|
||||
current := pm.ActivePageName()
|
||||
for i, name := range pageNames {
|
||||
if name == current {
|
||||
prev := (i - 1 + len(pageNames)) % len(pageNames)
|
||||
return pm.ActivatePage(pageNames[prev])
|
||||
}
|
||||
}
|
||||
if len(pageNames) > 0 {
|
||||
return pm.ActivatePage(pageNames[0])
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown page action: %s", action)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deckBuiltinAction(action string, deck *deck.Deck) error {
|
||||
switch action {
|
||||
case "brightness-up":
|
||||
current := deck.Brightness()
|
||||
next := current + 25
|
||||
if next > 100 {
|
||||
next = 25
|
||||
}
|
||||
return deck.SetBrightness(next)
|
||||
case "brightness-down":
|
||||
current := deck.Brightness()
|
||||
next := current - 25
|
||||
if next < 25 {
|
||||
next = 100
|
||||
}
|
||||
return deck.SetBrightness(next)
|
||||
default:
|
||||
return fmt.Errorf("unknown deck action: %s", action)
|
||||
}
|
||||
}
|
||||
|
||||
func keyboardTool() string {
|
||||
if os.Getenv("WAYLAND_DISPLAY") != "" {
|
||||
if _, err := exec.LookPath("ydotool"); err == nil {
|
||||
return "ydotool"
|
||||
}
|
||||
if _, err := exec.LookPath("wtype"); err == nil {
|
||||
return "wtype"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
if _, err := exec.LookPath("xdotool"); err == nil {
|
||||
return "xdotool"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func execKeyboard(a *config.Action) error {
|
||||
tool := keyboardTool()
|
||||
if tool == "" {
|
||||
wayland := os.Getenv("WAYLAND_DISPLAY") != ""
|
||||
if wayland {
|
||||
return fmt.Errorf("keyboard action requires wtype — install it (e.g. apk add wtype) and restart")
|
||||
}
|
||||
return fmt.Errorf("keyboard action requires xdotool — install it (e.g. apk add xdotool) and restart")
|
||||
}
|
||||
|
||||
keys := strings.ToLower(strings.TrimSpace(a.Keys))
|
||||
if keys == "" {
|
||||
return fmt.Errorf("keyboard action: keys is empty")
|
||||
}
|
||||
|
||||
parts := strings.Split(keys, "+")
|
||||
if len(parts) == 0 {
|
||||
return fmt.Errorf("keyboard action: invalid keys format %q", a.Keys)
|
||||
}
|
||||
|
||||
switch tool {
|
||||
case "ydotool":
|
||||
return execYDOTool(keys)
|
||||
case "wtype":
|
||||
return execWType(parts)
|
||||
case "xdotool":
|
||||
return execXDoTool(keys)
|
||||
default:
|
||||
return fmt.Errorf("keyboard action: unsupported tool %q", tool)
|
||||
}
|
||||
}
|
||||
|
||||
func execWType(parts []string) error {
|
||||
mainKey := parts[len(parts)-1]
|
||||
mods := parts[:len(parts)-1]
|
||||
|
||||
args := make([]string, 0, 2+len(mods)*2)
|
||||
|
||||
for _, m := range mods {
|
||||
args = append(args, "-M", m)
|
||||
}
|
||||
args = append(args, "-P", mainKey, "-p", mainKey)
|
||||
for i := len(mods) - 1; i >= 0; i-- {
|
||||
args = append(args, "-m", mods[i])
|
||||
}
|
||||
|
||||
cmd := exec.Command("wtype", args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func execXDoTool(keys string) error {
|
||||
cmd := exec.Command("xdotool", "key", keys)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func execYDOTool(keys string) error {
|
||||
// Use 'type' command instead of 'key' for better compatibility with Wine/games on Wayland
|
||||
cmd := exec.Command("ydotool", "type", keys)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
Reference in New Issue
Block a user