Multi-device support, dynamic grid adaptation, keyboard action
- Multi-device: OpenAllDecks(), GET /api/decks, per-deck PageManagers - Dynamic grid: frontend adapts to connected device layout - Keyboard action: new action type for sending key combos via wtype/xdotool - Key recorder UI: click-based modifier toggles + single key capture - Config: remove hardcoded maxKey validation - Screensaver and auto-switch sync across all decks
This commit is contained in:
parent
4c98a9d670
commit
dfe6544686
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@ streamdeck-lets-go
|
|||||||
*.out
|
*.out
|
||||||
*.exe
|
*.exe
|
||||||
/images/
|
/images/
|
||||||
|
PLAN.md
|
||||||
|
|||||||
106
daemon.go
106
daemon.go
@ -37,11 +37,11 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var deck *Deck
|
var decks []*Deck
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
for {
|
for {
|
||||||
deck, err = OpenDeck("")
|
decks, err = OpenAllDecks()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -52,18 +52,34 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
|
|||||||
case <-time.After(5 * time.Second):
|
case <-time.After(5 * time.Second):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defer deck.Close()
|
|
||||||
|
|
||||||
deck.SetBrightness(deviceBrightness(cfg))
|
pageMgrs := make([]*PageManager, len(decks))
|
||||||
|
for i, d := range decks {
|
||||||
|
pageMgrs[i] = NewPageManager(d)
|
||||||
|
pageMgrs[i].LoadPages(cfg.Pages)
|
||||||
|
}
|
||||||
|
primaryPM := pageMgrs[0]
|
||||||
|
primaryDeck := decks[0]
|
||||||
|
|
||||||
pm := NewPageManager(deck)
|
defer func() {
|
||||||
pm.LoadPages(cfg.Pages)
|
for _, d := range decks {
|
||||||
web.SetPageManager(pm)
|
d.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, d := range decks {
|
||||||
|
d.SetBrightness(deviceBrightness(cfg, d.Serial()))
|
||||||
|
}
|
||||||
|
|
||||||
|
web.SetDecks(decks)
|
||||||
|
web.SetPageManager(primaryPM)
|
||||||
|
|
||||||
|
for _, pm := range pageMgrs {
|
||||||
if err := pm.ActivatePage(cfg.DefaultPage); err != nil {
|
if err := pm.ActivatePage(cfg.DefaultPage); err != nil {
|
||||||
slog.Warn("activate default page", "error", err)
|
slog.Warn("activate default page", "error", err)
|
||||||
}
|
}
|
||||||
pm.startPeriodicKeys()
|
pm.startPeriodicKeys()
|
||||||
|
}
|
||||||
|
|
||||||
var windowCh <-chan Window
|
var windowCh <-chan Window
|
||||||
var detector WindowDetector
|
var detector WindowDetector
|
||||||
@ -93,20 +109,23 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
|
|||||||
defer configTicker.Stop()
|
defer configTicker.Stop()
|
||||||
var lastConfigMod time.Time
|
var lastConfigMod time.Time
|
||||||
|
|
||||||
slog.Info("daemon started")
|
slog.Info("daemon started", "decks", len(decks))
|
||||||
defer slog.Info("daemon stopped")
|
defer slog.Info("daemon stopped")
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case evt, ok := <-deck.Events():
|
case evt, ok := <-primaryDeck.Events():
|
||||||
if !ok {
|
if !ok {
|
||||||
slog.Warn("deck event channel closed, attempting reconnect...")
|
slog.Warn("deck event channel closed, attempting reconnect...")
|
||||||
deck.Close()
|
primaryDeck.Close()
|
||||||
deck = reconnectDeck(ctx, cfg, &pm, asm)
|
newDeck := reconnectDeck(ctx, cfg, &primaryPM, asm)
|
||||||
if deck == nil {
|
if newDeck == nil {
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
deck.SetBrightness(deviceBrightness(cfg))
|
decks[0] = newDeck
|
||||||
|
primaryDeck = newDeck
|
||||||
|
web.SetDecks(decks)
|
||||||
|
newDeck.SetBrightness(deviceBrightness(cfg, newDeck.Serial()))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,22 +134,24 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
|
|||||||
|
|
||||||
if evt.Kind == EventKeyPressed {
|
if evt.Kind == EventKeyPressed {
|
||||||
if wasSsActive {
|
if wasSsActive {
|
||||||
ssCtrl.Deactivate(deck)
|
ssCtrl.Deactivate(primaryDeck)
|
||||||
|
|
||||||
savedOutputs := pm.GetDisplayOutputs()
|
savedOutputs := primaryPM.GetDisplayOutputs()
|
||||||
|
|
||||||
if page := pm.ActivePage(); page != nil {
|
if page := primaryPM.ActivePage(); page != nil {
|
||||||
if err := pm.ActivatePage(pm.ActivePageName()); err != nil {
|
for _, pm := range pageMgrs {
|
||||||
|
if err := pm.ActivatePage(primaryPM.ActivePageName()); err != nil {
|
||||||
slog.Warn("screensaver: re-render page", "error", err)
|
slog.Warn("screensaver: re-render page", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for idx, output := range savedOutputs {
|
for idx, output := range savedOutputs {
|
||||||
pm.ReRenderDisplayKey(idx, output)
|
primaryPM.ReRenderDisplayKey(idx, output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
page := pm.ActivePage()
|
page := primaryPM.ActivePage()
|
||||||
if page == nil {
|
if page == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -143,9 +164,14 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
go func(a *config.Action) {
|
go func(a *config.Action) {
|
||||||
if err := ExecuteAction(a, deck, pm); err != nil {
|
if err := ExecuteAction(a, primaryDeck, primaryPM); err != nil {
|
||||||
slog.Error("execute action", "error", err)
|
slog.Error("execute action", "error", err)
|
||||||
}
|
}
|
||||||
|
if a.Type == "page" {
|
||||||
|
for _, pm := range pageMgrs[1:] {
|
||||||
|
pm.ActivatePage(a.Page)
|
||||||
|
}
|
||||||
|
}
|
||||||
}(k.Action)
|
}(k.Action)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -153,12 +179,14 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case win := <-windowCh:
|
case win := <-windowCh:
|
||||||
if page, ok := asm.Evaluate(win, pm.ActivePageName()); ok {
|
if page, ok := asm.Evaluate(win, primaryPM.ActivePageName()); ok {
|
||||||
|
for _, pm := range pageMgrs {
|
||||||
if err := pm.ActivatePage(page); err != nil {
|
if err := pm.ActivatePage(page); err != nil {
|
||||||
slog.Warn("auto-switch: activate page", "error", err)
|
slog.Warn("auto-switch: activate page", "error", err)
|
||||||
}
|
}
|
||||||
pm.startPeriodicKeys()
|
pm.startPeriodicKeys()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case <-configTicker.C:
|
case <-configTicker.C:
|
||||||
path := config.ConfigPath(opts.ConfigPath)
|
path := config.ConfigPath(opts.ConfigPath)
|
||||||
@ -178,37 +206,48 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
|
|||||||
cfg = newCfg
|
cfg = newCfg
|
||||||
web.UpdateConfig(cfg)
|
web.UpdateConfig(cfg)
|
||||||
ssCtrl = NewScreensaver(&cfg.Screensaver)
|
ssCtrl = NewScreensaver(&cfg.Screensaver)
|
||||||
deck.SetBrightness(deviceBrightness(cfg))
|
for _, d := range decks {
|
||||||
|
d.SetBrightness(deviceBrightness(cfg, d.Serial()))
|
||||||
|
}
|
||||||
|
for _, pm := range pageMgrs {
|
||||||
pm.stopPeriodicKeys()
|
pm.stopPeriodicKeys()
|
||||||
pm.LoadPages(cfg.Pages)
|
pm.LoadPages(cfg.Pages)
|
||||||
|
}
|
||||||
asm.Reload(cfg.AutoSwitch)
|
asm.Reload(cfg.AutoSwitch)
|
||||||
if len(cfg.AutoSwitch) > 0 && detector == nil {
|
if len(cfg.AutoSwitch) > 0 && detector == nil {
|
||||||
detector = NewWindowDetector()
|
detector = NewWindowDetector()
|
||||||
windowCh, _ = detector.Start(ctx)
|
windowCh, _ = detector.Start(ctx)
|
||||||
}
|
}
|
||||||
|
for _, pm := range pageMgrs {
|
||||||
if err := pm.ActivatePage(cfg.DefaultPage); err != nil {
|
if err := pm.ActivatePage(cfg.DefaultPage); err != nil {
|
||||||
slog.Warn("reload: activate default page", "error", err)
|
slog.Warn("reload: activate default page", "error", err)
|
||||||
}
|
}
|
||||||
pm.startPeriodicKeys()
|
pm.startPeriodicKeys()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if lastConfigMod.IsZero() {
|
if lastConfigMod.IsZero() {
|
||||||
lastConfigMod = mt
|
lastConfigMod = mt
|
||||||
}
|
}
|
||||||
|
|
||||||
case <-reconnectTicker.C:
|
case <-reconnectTicker.C:
|
||||||
if err := deck.SetBrightness(deck.Brightness()); err != nil {
|
if err := primaryDeck.SetBrightness(primaryDeck.Brightness()); err != nil {
|
||||||
slog.Warn("deck connection lost, reconnecting...", "error", err)
|
slog.Warn("deck connection lost, reconnecting...", "error", err)
|
||||||
deck.Close()
|
primaryDeck.Close()
|
||||||
deck = reconnectDeck(ctx, cfg, &pm, asm)
|
newDeck := reconnectDeck(ctx, cfg, &primaryPM, asm)
|
||||||
if deck == nil {
|
if newDeck == nil {
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
deck.SetBrightness(deviceBrightness(cfg))
|
decks[0] = newDeck
|
||||||
|
primaryDeck = newDeck
|
||||||
|
web.SetDecks(decks)
|
||||||
|
newDeck.SetBrightness(deviceBrightness(cfg, newDeck.Serial()))
|
||||||
}
|
}
|
||||||
|
|
||||||
case <-ssTicker.C:
|
case <-ssTicker.C:
|
||||||
if ssCtrl.Check() {
|
if ssCtrl.Check() {
|
||||||
ssCtrl.Activate(deck, &cfg.Screensaver)
|
for _, d := range decks {
|
||||||
|
ssCtrl.Activate(d, &cfg.Screensaver)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@ -229,9 +268,14 @@ func checkKeyboardTool() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func deviceBrightness(cfg *config.Config) int {
|
func deviceBrightness(cfg *config.Config, serial string) int {
|
||||||
if len(cfg.Devices) > 0 && cfg.Devices[0].Brightness > 0 {
|
for _, d := range cfg.Devices {
|
||||||
return cfg.Devices[0].Brightness
|
if d.Serial == serial || d.Serial == "" {
|
||||||
|
if d.Brightness > 0 {
|
||||||
|
return d.Brightness
|
||||||
|
}
|
||||||
|
return 75
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return 75
|
return 75
|
||||||
}
|
}
|
||||||
|
|||||||
45
deck.go
45
deck.go
@ -163,6 +163,51 @@ func (d *Deck) Events() <-chan Event { return d.events }
|
|||||||
func (d *Deck) NumKeys() int { return d.cfg.KeysX * d.cfg.KeysY }
|
func (d *Deck) NumKeys() int { return d.cfg.KeysX * d.cfg.KeysY }
|
||||||
func (d *Deck) KeySize() int { return d.cfg.KeySize }
|
func (d *Deck) KeySize() int { return d.cfg.KeySize }
|
||||||
func (d *Deck) Config() DeckConfig { return d.cfg }
|
func (d *Deck) Config() DeckConfig { return d.cfg }
|
||||||
|
func (d *Deck) Model() string { return d.cfg.Name }
|
||||||
|
|
||||||
|
type DeckInfo struct {
|
||||||
|
Serial string `json:"serial"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
KeysX int `json:"keys_x"`
|
||||||
|
KeysY int `json:"keys_y"`
|
||||||
|
NumKeys int `json:"num_keys"`
|
||||||
|
KeySize int `json:"key_size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Deck) DeckInfo() DeckInfo {
|
||||||
|
return DeckInfo{
|
||||||
|
Serial: d.serial,
|
||||||
|
Model: d.cfg.Name,
|
||||||
|
KeysX: d.cfg.KeysX,
|
||||||
|
KeysY: d.cfg.KeysY,
|
||||||
|
NumKeys: d.cfg.KeysX * d.cfg.KeysY,
|
||||||
|
KeySize: d.cfg.KeySize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func OpenAllDecks() ([]*Deck, error) {
|
||||||
|
devices := hid.Enumerate(streamdeck.VendorID, 0)
|
||||||
|
if len(devices) == 0 {
|
||||||
|
return nil, fmt.Errorf("no stream deck devices found")
|
||||||
|
}
|
||||||
|
var decks []*Deck
|
||||||
|
for _, d := range devices {
|
||||||
|
if _, ok := findConfig(d.ProductID); !ok {
|
||||||
|
slog.Warn("skipping unsupported device", "pid", fmt.Sprintf("0x%04x", d.ProductID))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
deck, err := OpenDeck(d.Serial)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("failed to open deck", "serial", d.Serial, "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
decks = append(decks, deck)
|
||||||
|
}
|
||||||
|
if len(decks) == 0 {
|
||||||
|
return nil, fmt.Errorf("no supported stream deck devices could be opened")
|
||||||
|
}
|
||||||
|
return decks, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Deck) SetBrightness(val int) error {
|
func (d *Deck) SetBrightness(val int) error {
|
||||||
if val < 0 {
|
if val < 0 {
|
||||||
|
|||||||
@ -160,14 +160,10 @@ func (c *Config) Validate() error {
|
|||||||
pageNames[p.Name] = true
|
pageNames[p.Name] = true
|
||||||
|
|
||||||
seenKeys := make(map[int]bool)
|
seenKeys := make(map[int]bool)
|
||||||
maxKey := 31
|
|
||||||
for _, k := range p.Keys {
|
for _, k := range p.Keys {
|
||||||
if k.Index < 0 {
|
if k.Index < 0 {
|
||||||
return fmt.Errorf("page %s: key index must be >= 0", p.Name)
|
return fmt.Errorf("page %s: key index must be >= 0", p.Name)
|
||||||
}
|
}
|
||||||
if k.Index > maxKey {
|
|
||||||
return fmt.Errorf("page %s: key index %d exceeds max %d", p.Name, k.Index, maxKey)
|
|
||||||
}
|
|
||||||
if seenKeys[k.Index] {
|
if seenKeys[k.Index] {
|
||||||
return fmt.Errorf("page %s: duplicate key index %d", p.Name, k.Index)
|
return fmt.Errorf("page %s: duplicate key index %d", p.Name, k.Index)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,8 @@ document.addEventListener('alpine:init', () => {
|
|||||||
toast: null,
|
toast: null,
|
||||||
displayOutputs: {},
|
displayOutputs: {},
|
||||||
isCapturingKey: false,
|
isCapturingKey: false,
|
||||||
|
decks: [],
|
||||||
|
activeDeckSerial: '',
|
||||||
|
|
||||||
// ── Edit state ──
|
// ── Edit state ──
|
||||||
editing: null,
|
editing: null,
|
||||||
@ -25,9 +27,22 @@ document.addEventListener('alpine:init', () => {
|
|||||||
// ── Init ──
|
// ── Init ──
|
||||||
async init() {
|
async init() {
|
||||||
await this.loadConfig()
|
await this.loadConfig()
|
||||||
|
await this.loadDecks()
|
||||||
setInterval(() => this.pollDisplayOutputs(), 3000)
|
setInterval(() => this.pollDisplayOutputs(), 3000)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async loadDecks() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/decks')
|
||||||
|
if (res.ok) {
|
||||||
|
this.decks = await res.json()
|
||||||
|
if (this.decks.length > 0 && !this.activeDeckSerial) {
|
||||||
|
this.activeDeckSerial = this.decks[0].serial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
},
|
||||||
|
|
||||||
async pollDisplayOutputs() {
|
async pollDisplayOutputs() {
|
||||||
if (this.view !== 'grid') return
|
if (this.view !== 'grid') return
|
||||||
try {
|
try {
|
||||||
@ -69,12 +84,18 @@ document.addEventListener('alpine:init', () => {
|
|||||||
return this.pages.map(p => p.name)
|
return this.pages.map(p => p.name)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get activeDeck() {
|
||||||
|
return this.decks.find(d => d.serial === this.activeDeckSerial)
|
||||||
|
|| { keys_x: 5, keys_y: 3, num_keys: 15 }
|
||||||
|
},
|
||||||
|
|
||||||
// ── Grid ──
|
// ── Grid ──
|
||||||
get gridKeys() {
|
get gridKeys() {
|
||||||
const page = this.currentPage
|
const page = this.currentPage
|
||||||
if (!page) return []
|
if (!page) return []
|
||||||
const keys = []
|
const keys = []
|
||||||
for (let i = 0; i < 15; i++) {
|
const n = this.activeDeck.num_keys
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
const kc = page.keys.find(k => k.index === i)
|
const kc = page.keys.find(k => k.index === i)
|
||||||
keys.push({
|
keys.push({
|
||||||
index: i,
|
index: i,
|
||||||
|
|||||||
@ -45,7 +45,15 @@
|
|||||||
|
|
||||||
<!-- ═══ Grid View ═══ -->
|
<!-- ═══ Grid View ═══ -->
|
||||||
<main x-show="view === 'grid' && !subView">
|
<main x-show="view === 'grid' && !subView">
|
||||||
<div class="deck-grid">
|
<div class="deck-selector" x-show="decks.length > 1">
|
||||||
|
<template x-for="d in decks" :key="d.serial">
|
||||||
|
<button class="deck-btn"
|
||||||
|
:class="{ active: d.serial === activeDeckSerial }"
|
||||||
|
@click="activeDeckSerial = d.serial"
|
||||||
|
x-text="d.model + ' (' + d.keys_x + '\u00d7' + d.keys_y + ')'"></button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="deck-grid" :style="'grid-template-columns: repeat(' + activeDeck.keys_x + ', 1fr)'">
|
||||||
<template x-for="key in gridKeys" :key="key.index">
|
<template x-for="key in gridKeys" :key="key.index">
|
||||||
<button class="key-btn"
|
<button class="key-btn"
|
||||||
:class="{ empty: !key.configured }"
|
:class="{ empty: !key.configured }"
|
||||||
|
|||||||
@ -107,7 +107,6 @@ main {
|
|||||||
|
|
||||||
.deck-grid {
|
.deck-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(5, 1fr);
|
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
@ -115,6 +114,33 @@ main {
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.deck-selector {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
.deck-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-xs);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.deck-btn:hover { color: var(--text); border-color: var(--border-light); }
|
||||||
|
.deck-btn.active {
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
.key-btn {
|
.key-btn {
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
|
|||||||
22
web.go
22
web.go
@ -37,6 +37,7 @@ type WebServer struct {
|
|||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
configPath string
|
configPath string
|
||||||
pm *PageManager
|
pm *PageManager
|
||||||
|
decks []*Deck
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,6 +57,12 @@ func (s *WebServer) SetPageManager(pm *PageManager) {
|
|||||||
s.pm = pm
|
s.pm = pm
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *WebServer) SetDecks(decks []*Deck) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.decks = decks
|
||||||
|
}
|
||||||
|
|
||||||
func (s *WebServer) Serve(ctx context.Context, addr string) error {
|
func (s *WebServer) Serve(ctx context.Context, addr string) error {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
@ -75,6 +82,8 @@ func (s *WebServer) Serve(ctx context.Context, addr string) error {
|
|||||||
|
|
||||||
mux.HandleFunc("GET /api/models", s.handleGetModels)
|
mux.HandleFunc("GET /api/models", s.handleGetModels)
|
||||||
|
|
||||||
|
mux.HandleFunc("GET /api/decks", s.handleGetDecks)
|
||||||
|
|
||||||
mux.HandleFunc("GET /api/status", s.handleGetStatus)
|
mux.HandleFunc("GET /api/status", s.handleGetStatus)
|
||||||
|
|
||||||
mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticRoot))))
|
mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticRoot))))
|
||||||
@ -471,6 +480,19 @@ func (s *WebServer) handleGetModels(w http.ResponseWriter, r *http.Request) {
|
|||||||
json.NewEncoder(w).Encode(models)
|
json.NewEncoder(w).Encode(models)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *WebServer) handleGetDecks(w http.ResponseWriter, r *http.Request) {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
infos := make([]DeckInfo, 0, len(s.decks))
|
||||||
|
for _, d := range s.decks {
|
||||||
|
infos = append(infos, d.DeckInfo())
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(infos)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *WebServer) handleGetStatus(w http.ResponseWriter, r *http.Request) {
|
func (s *WebServer) handleGetStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user