Add folder support: keys that open sub-pages with a back key
A key with `folder: NAME` swaps the deck to the keys defined under `folders.NAME`. Every folder shows a back key (slot 0 by default, drawn with a built-in chevron) that pops back to the parent; folders nest. The daemon now runs one page at a time: per-key setup moved into setupKey(), openPage() draws a page under its own context, and page.stop() tears it down before the next page is drawn (and on exit, so device death no longer waits on uncancelled goroutines). streamdeck-ctl gains `key set -folder`, reports kind "folder", lists every folder in status, and refuses to `key press` a navigation-only key. The Omarchy grid shows a folder glyph. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ARLa4moGb154fzc9TevgV
This commit is contained in:
@@ -30,6 +30,7 @@ type Status struct {
|
||||
Daemon DaemonStatus `json:"daemon"`
|
||||
Device DeviceStatus `json:"device"`
|
||||
Keys []KeyStatus `json:"keys"`
|
||||
Folders []FolderStatus `json:"folders"`
|
||||
Icons []string `json:"icons"`
|
||||
Modules []ModuleStatus `json:"modules"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
@@ -62,7 +63,7 @@ type DeviceStatus struct {
|
||||
// shape comes from Device.Cols/Rows so a front-end can lay out empty slots.
|
||||
type KeyStatus struct {
|
||||
Index int `json:"index"`
|
||||
Kind string `json:"kind"` // empty|static|text|toggle|module
|
||||
Kind string `json:"kind"` // empty|static|text|toggle|module|folder
|
||||
Label string `json:"label"` // short human label for a grid cell
|
||||
Icon string `json:"icon,omitempty"`
|
||||
IconPath string `json:"iconPath,omitempty"` // absolute, for previews
|
||||
@@ -78,6 +79,17 @@ type KeyStatus struct {
|
||||
Params map[string]string `json:"params,omitempty"`
|
||||
Privileged bool `json:"privileged"`
|
||||
Poll *PollStatus `json:"poll,omitempty"`
|
||||
Folder string `json:"folder,omitempty"` // folder the key opens on press
|
||||
}
|
||||
|
||||
// FolderStatus is one sub-page from the folders: block, with the same key
|
||||
// shape as the root page so a front-end can render it with the same grid.
|
||||
type FolderStatus struct {
|
||||
Name string `json:"name"`
|
||||
BackKey int `json:"backKey"`
|
||||
BackIcon string `json:"backIcon,omitempty"`
|
||||
BackText string `json:"backText,omitempty"`
|
||||
Keys []KeyStatus `json:"keys"`
|
||||
}
|
||||
|
||||
type PollStatus struct {
|
||||
@@ -104,6 +116,7 @@ func cmdStatus(cfgPath string) error {
|
||||
ConfigPath: cfgPath,
|
||||
ModulesPath: config.ModulesPath(cfgPath),
|
||||
Keys: []KeyStatus{},
|
||||
Folders: []FolderStatus{},
|
||||
Icons: []string{},
|
||||
Modules: []ModuleStatus{},
|
||||
}
|
||||
@@ -131,6 +144,7 @@ func cmdStatus(cfgPath string) error {
|
||||
}
|
||||
status.Modules = moduleList(reg)
|
||||
status.Keys = keyList(cfg, reg)
|
||||
status.Folders = folderList(cfg, reg)
|
||||
status.Icons = iconList(cfg.IconsDir)
|
||||
|
||||
emit(status)
|
||||
@@ -138,15 +152,41 @@ func cmdStatus(cfgPath string) error {
|
||||
}
|
||||
|
||||
func keyList(cfg *config.Config, reg *modules.Registry) []KeyStatus {
|
||||
indices := make([]int, 0, len(cfg.Keys))
|
||||
for index := range cfg.Keys {
|
||||
return keyEntries(cfg.Keys, cfg.IconsDir, reg)
|
||||
}
|
||||
|
||||
// folderList renders every folder in name order.
|
||||
func folderList(cfg *config.Config, reg *modules.Registry) []FolderStatus {
|
||||
names := make([]string, 0, len(cfg.Folders))
|
||||
for name := range cfg.Folders {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
out := make([]FolderStatus, 0, len(names))
|
||||
for _, name := range names {
|
||||
f := cfg.Folders[name]
|
||||
out = append(out, FolderStatus{
|
||||
Name: name,
|
||||
BackKey: f.BackKey(),
|
||||
BackIcon: f.Back.Icon,
|
||||
BackText: f.Back.Text,
|
||||
Keys: keyEntries(f.Keys, cfg.IconsDir, reg),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func keyEntries(keys map[int]config.KeyConfig, iconsDir string, reg *modules.Registry) []KeyStatus {
|
||||
indices := make([]int, 0, len(keys))
|
||||
for index := range keys {
|
||||
indices = append(indices, index)
|
||||
}
|
||||
sort.Ints(indices)
|
||||
|
||||
out := make([]KeyStatus, 0, len(indices))
|
||||
for _, index := range indices {
|
||||
key := cfg.Keys[index]
|
||||
key := keys[index]
|
||||
entry := KeyStatus{
|
||||
Index: index,
|
||||
Icon: key.Icon,
|
||||
@@ -160,10 +200,11 @@ func keyList(cfg *config.Config, reg *modules.Registry) []KeyStatus {
|
||||
Module: key.Module,
|
||||
Function: key.Function,
|
||||
Params: key.Params,
|
||||
Folder: key.Folder,
|
||||
}
|
||||
|
||||
if icon := firstNonEmpty(key.Icon, key.IconTrue, key.IconFalse); icon != "" {
|
||||
entry.IconPath = resolveIcon(cfg.IconsDir, icon)
|
||||
entry.IconPath = resolveIcon(iconsDir, icon)
|
||||
}
|
||||
|
||||
// The rendered template can inline secrets ({{env "TOKEN"}}), so it never
|
||||
@@ -194,6 +235,8 @@ func keyList(cfg *config.Config, reg *modules.Registry) []KeyStatus {
|
||||
|
||||
func keyKind(key config.KeyConfig) string {
|
||||
switch {
|
||||
case key.Folder != "":
|
||||
return "folder"
|
||||
case key.Poll != nil:
|
||||
return "toggle"
|
||||
case key.Module != "":
|
||||
@@ -215,6 +258,9 @@ func keyLabel(key config.KeyConfig) string {
|
||||
return strings.TrimSpace(line)
|
||||
}
|
||||
}
|
||||
if key.Folder != "" {
|
||||
return key.Folder
|
||||
}
|
||||
if key.Function != "" {
|
||||
return strings.ReplaceAll(key.Function, "_", " ")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user