1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

matugen: fix adw-gtk3 setting in light mode

- and add models.Get/GetOr helpers
This commit is contained in:
bbedward
2026-01-01 23:13:12 -05:00
parent 5e111d89a5
commit c1d57946d9
23 changed files with 162 additions and 132 deletions

View File

@@ -16,6 +16,13 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
type ColorMode string
const (
ColorModeDark ColorMode = "dark"
ColorModeLight ColorMode = "light"
)
var (
matugenVersionOnce sync.Once
matugenSupportsCOE bool
@@ -27,7 +34,7 @@ type Options struct {
ConfigDir string
Kind string
Value string
Mode string
Mode ColorMode
IconTheme string
MatugenType string
RunUserTemplates bool
@@ -77,7 +84,7 @@ func Run(opts Options) error {
return fmt.Errorf("value is required")
}
if opts.Mode == "" {
opts.Mode = "dark"
opts.Mode = ColorModeDark
}
if opts.MatugenType == "" {
opts.MatugenType = "scheme-tonal-spot"
@@ -145,7 +152,7 @@ func buildOnce(opts *Options) error {
importArgs = []string{"--import-json-string", importData}
log.Info("Running matugen color hex with stock color overrides")
args := []string{"color", "hex", primaryDark, "-m", opts.Mode, "-t", opts.MatugenType, "-c", cfgFile.Name()}
args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()}
args = append(args, importArgs...)
if err := runMatugen(args); err != nil {
return err
@@ -181,7 +188,7 @@ func buildOnce(opts *Options) error {
default:
args = []string{opts.Kind, opts.Value}
}
args = append(args, "-m", opts.Mode, "-t", opts.MatugenType, "-c", cfgFile.Name())
args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name())
args = append(args, importArgs...)
if err := runMatugen(args); err != nil {
return err
@@ -556,19 +563,19 @@ func extractNestedColor(jsonStr, colorName, variant string) string {
return color
}
func generateDank16Variants(primaryDark, primaryLight, surface, mode string) string {
func generateDank16Variants(primaryDark, primaryLight, surface string, mode ColorMode) string {
variantOpts := dank16.VariantOptions{
PrimaryDark: primaryDark,
PrimaryLight: primaryLight,
Background: surface,
UseDPS: true,
IsLightMode: mode == "light",
IsLightMode: mode == ColorModeLight,
}
variantColors := dank16.GenerateVariantPalette(variantOpts)
return dank16.GenerateVariantJSON(variantColors)
}
func refreshGTK(configDir, mode string) {
func refreshGTK(configDir string, mode ColorMode) {
gtkCSS := filepath.Join(configDir, "gtk-3.0", "gtk.css")
info, err := os.Lstat(gtkCSS)
@@ -593,8 +600,16 @@ func refreshGTK(configDir, mode string) {
return
}
var gtk3Theme string
switch mode {
case ColorModeDark:
gtk3Theme = "adw-gtk3-dark"
default:
gtk3Theme = "adw-gtk3"
}
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", "").Run()
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", "adw-gtk3-"+mode).Run()
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", gtk3Theme).Run()
}
func signalTerminals() {
@@ -624,9 +639,9 @@ func signalByName(name string, sig syscall.Signal) {
}
}
func syncColorScheme(mode string) {
func syncColorScheme(mode ColorMode) {
scheme := "prefer-dark"
if mode == "light" {
if mode == ColorModeLight {
scheme = "default"
}

View File

@@ -19,9 +19,9 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
func handleOpen(conn net.Conn, req models.Request, manager *Manager) {
log.Infof("AppPicker: Received %s request with params: %+v", req.Method, req.Params)
target, ok := req.Params["target"].(string)
target, ok := models.Get[string](req, "target")
if !ok {
target, ok = req.Params["url"].(string)
target, ok = models.Get[string](req, "url")
if !ok {
log.Warnf("AppPicker: Invalid target parameter in request")
models.RespondError(conn, req.ID, "invalid target parameter")
@@ -31,14 +31,11 @@ func handleOpen(conn net.Conn, req models.Request, manager *Manager) {
event := OpenEvent{
Target: target,
RequestType: "url",
RequestType: models.GetOr(req, "requestType", "url"),
MimeType: models.GetOr(req, "mimeType", ""),
}
if mimeType, ok := req.Params["mimeType"].(string); ok {
event.MimeType = mimeType
}
if categories, ok := req.Params["categories"].([]any); ok {
if categories, ok := models.Get[[]any](req, "categories"); ok {
event.Categories = make([]string, 0, len(categories))
for _, cat := range categories {
if catStr, ok := cat.(string); ok {
@@ -47,10 +44,6 @@ func handleOpen(conn net.Conn, req models.Request, manager *Manager) {
}
}
if requestType, ok := req.Params["requestType"].(string); ok {
event.RequestType = requestType
}
log.Infof("AppPicker: Broadcasting event: %+v", event)
manager.RequestOpen(event)
models.Respond(conn, req.ID, "ok")

View File

@@ -9,7 +9,7 @@ import (
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
switch req.Method {
case "browser.open":
url, ok := req.Params["url"].(string)
url, ok := models.Get[string](req, "url")
if !ok {
models.RespondError(conn, req.ID, "invalid url parameter")
return

View File

@@ -168,14 +168,14 @@ func handleSearch(conn net.Conn, req models.Request, m *Manager) {
Offset: params.IntOpt(req.Params, "offset", 0),
}
if img, ok := req.Params["isImage"].(bool); ok {
if img, ok := models.Get[bool](req, "isImage"); ok {
p.IsImage = &img
}
if b, ok := req.Params["before"].(float64); ok {
if b, ok := models.Get[float64](req, "before"); ok {
v := int64(b)
p.Before = &v
}
if a, ok := req.Params["after"].(float64); ok {
if a, ok := models.Get[float64](req, "after"); ok {
v := int64(a)
p.After = &v
}
@@ -190,19 +190,19 @@ func handleGetConfig(conn net.Conn, req models.Request, m *Manager) {
func handleSetConfig(conn net.Conn, req models.Request, m *Manager) {
cfg := m.GetConfig()
if _, ok := req.Params["maxHistory"]; ok {
cfg.MaxHistory = params.IntOpt(req.Params, "maxHistory", cfg.MaxHistory)
if v, ok := models.Get[float64](req, "maxHistory"); ok {
cfg.MaxHistory = int(v)
}
if _, ok := req.Params["maxEntrySize"]; ok {
cfg.MaxEntrySize = int64(params.IntOpt(req.Params, "maxEntrySize", int(cfg.MaxEntrySize)))
if v, ok := models.Get[float64](req, "maxEntrySize"); ok {
cfg.MaxEntrySize = int64(v)
}
if _, ok := req.Params["autoClearDays"]; ok {
cfg.AutoClearDays = params.IntOpt(req.Params, "autoClearDays", cfg.AutoClearDays)
if v, ok := models.Get[float64](req, "autoClearDays"); ok {
cfg.AutoClearDays = int(v)
}
if v, ok := req.Params["clearAtStartup"].(bool); ok {
if v, ok := models.Get[bool](req, "clearAtStartup"); ok {
cfg.ClearAtStartup = v
}
if v, ok := req.Params["disabled"].(bool); ok {
if v, ok := models.Get[bool](req, "disabled"); ok {
cfg.Disabled = v
}

View File

@@ -41,19 +41,19 @@ func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
}
func handleSetTags(conn net.Conn, req models.Request, manager *Manager) {
output, ok := req.Params["output"].(string)
output, ok := models.Get[string](req, "output")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
return
}
tagmask, ok := req.Params["tagmask"].(float64)
tagmask, ok := models.Get[float64](req, "tagmask")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'tagmask' parameter")
return
}
toggleTagset, ok := req.Params["toggleTagset"].(float64)
toggleTagset, ok := models.Get[float64](req, "toggleTagset")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'toggleTagset' parameter")
return
@@ -68,19 +68,19 @@ func handleSetTags(conn net.Conn, req models.Request, manager *Manager) {
}
func handleSetClientTags(conn net.Conn, req models.Request, manager *Manager) {
output, ok := req.Params["output"].(string)
output, ok := models.Get[string](req, "output")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
return
}
andTags, ok := req.Params["andTags"].(float64)
andTags, ok := models.Get[float64](req, "andTags")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'andTags' parameter")
return
}
xorTags, ok := req.Params["xorTags"].(float64)
xorTags, ok := models.Get[float64](req, "xorTags")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'xorTags' parameter")
return
@@ -95,13 +95,13 @@ func handleSetClientTags(conn net.Conn, req models.Request, manager *Manager) {
}
func handleSetLayout(conn net.Conn, req models.Request, manager *Manager) {
output, ok := req.Params["output"].(string)
output, ok := models.Get[string](req, "output")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
return
}
index, ok := req.Params["index"].(float64)
index, ok := models.Get[float64](req, "index")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'index' parameter")
return

View File

@@ -43,12 +43,8 @@ func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
}
func handleActivateWorkspace(conn net.Conn, req models.Request, manager *Manager) {
groupID, ok := req.Params["groupID"].(string)
if !ok {
groupID = ""
}
workspaceID, ok := req.Params["workspaceID"].(string)
groupID := models.GetOr(req, "groupID", "")
workspaceID, ok := models.Get[string](req, "workspaceID")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'workspaceID' parameter")
return
@@ -63,12 +59,8 @@ func handleActivateWorkspace(conn net.Conn, req models.Request, manager *Manager
}
func handleDeactivateWorkspace(conn net.Conn, req models.Request, manager *Manager) {
groupID, ok := req.Params["groupID"].(string)
if !ok {
groupID = ""
}
workspaceID, ok := req.Params["workspaceID"].(string)
groupID := models.GetOr(req, "groupID", "")
workspaceID, ok := models.Get[string](req, "workspaceID")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'workspaceID' parameter")
return
@@ -83,12 +75,8 @@ func handleDeactivateWorkspace(conn net.Conn, req models.Request, manager *Manag
}
func handleRemoveWorkspace(conn net.Conn, req models.Request, manager *Manager) {
groupID, ok := req.Params["groupID"].(string)
if !ok {
groupID = ""
}
workspaceID, ok := req.Params["workspaceID"].(string)
groupID := models.GetOr(req, "groupID", "")
workspaceID, ok := models.Get[string](req, "workspaceID")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'workspaceID' parameter")
return
@@ -103,13 +91,13 @@ func handleRemoveWorkspace(conn net.Conn, req models.Request, manager *Manager)
}
func handleCreateWorkspace(conn net.Conn, req models.Request, manager *Manager) {
groupID, ok := req.Params["groupID"].(string)
groupID, ok := models.Get[string](req, "groupID")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'groupID' parameter")
return
}
workspaceName, ok := req.Params["name"].(string)
workspaceName, ok := models.Get[string](req, "name")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
return

View File

@@ -15,37 +15,23 @@ type MatugenQueueResult struct {
}
func handleMatugenQueue(conn net.Conn, req models.Request) {
getString := func(key string) string {
if v, ok := req.Params[key].(string); ok {
return v
}
return ""
}
getBool := func(key string, def bool) bool {
if v, ok := req.Params[key].(bool); ok {
return v
}
return def
}
opts := matugen.Options{
StateDir: getString("stateDir"),
ShellDir: getString("shellDir"),
ConfigDir: getString("configDir"),
Kind: getString("kind"),
Value: getString("value"),
Mode: getString("mode"),
IconTheme: getString("iconTheme"),
MatugenType: getString("matugenType"),
RunUserTemplates: getBool("runUserTemplates", true),
StockColors: getString("stockColors"),
SyncModeWithPortal: getBool("syncModeWithPortal", false),
TerminalsAlwaysDark: getBool("terminalsAlwaysDark", false),
SkipTemplates: getString("skipTemplates"),
StateDir: models.GetOr(req, "stateDir", ""),
ShellDir: models.GetOr(req, "shellDir", ""),
ConfigDir: models.GetOr(req, "configDir", ""),
Kind: models.GetOr(req, "kind", ""),
Value: models.GetOr(req, "value", ""),
Mode: matugen.ColorMode(models.GetOr(req, "mode", "")),
IconTheme: models.GetOr(req, "iconTheme", ""),
MatugenType: models.GetOr(req, "matugenType", ""),
RunUserTemplates: models.GetOr(req, "runUserTemplates", true),
StockColors: models.GetOr(req, "stockColors", ""),
SyncModeWithPortal: models.GetOr(req, "syncModeWithPortal", false),
TerminalsAlwaysDark: models.GetOr(req, "terminalsAlwaysDark", false),
SkipTemplates: models.GetOr(req, "skipTemplates", ""),
}
wait := getBool("wait", true)
wait := models.GetOr(req, "wait", true)
queue := matugen.GetQueue()
resultCh := queue.Submit(opts)

View File

@@ -5,6 +5,7 @@ import (
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
)
type Request struct {
@@ -13,6 +14,15 @@ type Request struct {
Params map[string]any `json:"params,omitempty"`
}
func Get[T any](r Request, key string) (T, bool) {
v, err := params.Get[T](r.Params, key)
return v, err == nil
}
func GetOr[T any](r Request, key string, def T) T {
return params.GetOpt(r.Params, key, def)
}
type Response[T any] struct {
ID int `json:"id,omitempty"`
Result *T `json:"result,omitempty"`

View File

@@ -0,0 +1,52 @@
package models
import "testing"
func TestGet(t *testing.T) {
req := Request{Params: map[string]any{"name": "test", "count": 42, "enabled": true}}
name, ok := Get[string](req, "name")
if !ok || name != "test" {
t.Errorf("Get[string] = %q, %v; want 'test', true", name, ok)
}
count, ok := Get[int](req, "count")
if !ok || count != 42 {
t.Errorf("Get[int] = %d, %v; want 42, true", count, ok)
}
enabled, ok := Get[bool](req, "enabled")
if !ok || !enabled {
t.Errorf("Get[bool] = %v, %v; want true, true", enabled, ok)
}
_, ok = Get[string](req, "missing")
if ok {
t.Error("Get missing key should return false")
}
_, ok = Get[int](req, "name")
if ok {
t.Error("Get wrong type should return false")
}
}
func TestGetOr(t *testing.T) {
req := Request{Params: map[string]any{"name": "test", "enabled": true}}
if v := GetOr(req, "name", "default"); v != "test" {
t.Errorf("GetOr existing = %q; want 'test'", v)
}
if v := GetOr(req, "missing", "default"); v != "default" {
t.Errorf("GetOr missing = %q; want 'default'", v)
}
if v := GetOr(req, "enabled", false); !v {
t.Errorf("GetOr bool = %v; want true", v)
}
if v := GetOr(req, "name", 0); v != 0 {
t.Errorf("GetOr wrong type = %d; want 0 (default)", v)
}
}

View File

@@ -157,7 +157,7 @@ func handleConnectWiFi(conn net.Conn, req models.Request, manager *Manager) {
connReq.Username = params.StringOpt(req.Params, "username", "")
connReq.Device = params.StringOpt(req.Params, "device", "")
if interactive, ok := req.Params["interactive"].(bool); ok {
if interactive, ok := models.Get[bool](req, "interactive"); ok {
connReq.Interactive = interactive
} else {
state := manager.GetState()
@@ -185,7 +185,7 @@ func handleConnectWiFi(conn net.Conn, req models.Request, manager *Manager) {
connReq.ClientCertPath = params.StringOpt(req.Params, "clientCertPath", "")
connReq.PrivateKeyPath = params.StringOpt(req.Params, "privateKeyPath", "")
if useSystemCACerts, ok := req.Params["useSystemCACerts"].(bool); ok {
if useSystemCACerts, ok := models.Get[bool](req, "useSystemCACerts"); ok {
connReq.UseSystemCACerts = &useSystemCACerts
}
@@ -528,13 +528,13 @@ func handleUpdateVPNConfig(conn net.Conn, req models.Request, manager *Manager)
updates := make(map[string]any)
if name, ok := req.Params["name"].(string); ok {
if name, ok := models.Get[string](req, "name"); ok {
updates["name"] = name
}
if autoconnect, ok := req.Params["autoconnect"].(bool); ok {
if autoconnect, ok := models.Get[bool](req, "autoconnect"); ok {
updates["autoconnect"] = autoconnect
}
if data, ok := req.Params["data"].(map[string]any); ok {
if data, ok := models.Get[map[string]any](req, "data"); ok {
updates["data"] = data
}

View File

@@ -9,7 +9,7 @@ import (
)
func HandleInstall(conn net.Conn, req models.Request) {
idOrName, ok := req.Params["name"].(string)
idOrName, ok := models.Get[string](req, "name")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
return

View File

@@ -10,7 +10,7 @@ import (
)
func HandleSearch(conn net.Conn, req models.Request) {
query, ok := req.Params["query"].(string)
query, ok := models.Get[string](req, "query")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'query' parameter")
return
@@ -30,15 +30,15 @@ func HandleSearch(conn net.Conn, req models.Request) {
searchResults := plugins.FuzzySearch(query, pluginList)
if category, ok := req.Params["category"].(string); ok && category != "" {
if category := models.GetOr(req, "category", ""); category != "" {
searchResults = plugins.FilterByCategory(category, searchResults)
}
if compositor, ok := req.Params["compositor"].(string); ok && compositor != "" {
if compositor := models.GetOr(req, "compositor", ""); compositor != "" {
searchResults = plugins.FilterByCompositor(compositor, searchResults)
}
if capability, ok := req.Params["capability"].(string); ok && capability != "" {
if capability := models.GetOr(req, "capability", ""); capability != "" {
searchResults = plugins.FilterByCapability(capability, searchResults)
}

View File

@@ -9,7 +9,7 @@ import (
)
func HandleUninstall(conn net.Conn, req models.Request) {
name, ok := req.Params["name"].(string)
name, ok := models.Get[string](req, "name")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
return

View File

@@ -9,7 +9,7 @@ import (
)
func HandleUpdate(conn net.Conn, req models.Request) {
name, ok := req.Params["name"].(string)
name, ok := models.Get[string](req, "name")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
return

View File

@@ -192,19 +192,19 @@ func RouteRequest(conn net.Conn, req models.Request) {
func handleClipboardSetConfig(conn net.Conn, req models.Request) {
cfg := clipboard.LoadConfig()
if v, ok := req.Params["maxHistory"].(float64); ok {
if v, ok := models.Get[float64](req, "maxHistory"); ok {
cfg.MaxHistory = int(v)
}
if v, ok := req.Params["maxEntrySize"].(float64); ok {
if v, ok := models.Get[float64](req, "maxEntrySize"); ok {
cfg.MaxEntrySize = int64(v)
}
if v, ok := req.Params["autoClearDays"].(float64); ok {
if v, ok := models.Get[float64](req, "autoClearDays"); ok {
cfg.AutoClearDays = int(v)
}
if v, ok := req.Params["clearAtStartup"].(bool); ok {
if v, ok := models.Get[bool](req, "clearAtStartup"); ok {
cfg.ClearAtStartup = v
}
if v, ok := req.Params["disabled"].(bool); ok {
if v, ok := models.Get[bool](req, "disabled"); ok {
cfg.Disabled = v
}

View File

@@ -520,7 +520,7 @@ func handleSubscribe(conn net.Conn, req models.Request) {
clientID := fmt.Sprintf("meta-client-%p", conn)
var services []string
if servicesParam, ok := req.Params["services"].([]any); ok {
if servicesParam, ok := models.Get[[]any](req, "services"); ok {
for _, s := range servicesParam {
if str, ok := s.(string); ok {
services = append(services, str)

View File

@@ -9,7 +9,7 @@ import (
)
func HandleInstall(conn net.Conn, req models.Request) {
idOrName, ok := req.Params["name"].(string)
idOrName, ok := models.Get[string](req, "name")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
return

View File

@@ -9,7 +9,7 @@ import (
)
func HandleSearch(conn net.Conn, req models.Request) {
query, ok := req.Params["query"].(string)
query, ok := models.Get[string](req, "query")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'query' parameter")
return

View File

@@ -9,7 +9,7 @@ import (
)
func HandleUninstall(conn net.Conn, req models.Request) {
idOrName, ok := req.Params["name"].(string)
idOrName, ok := models.Get[string](req, "name")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
return

View File

@@ -9,7 +9,7 @@ import (
)
func HandleUpdate(conn net.Conn, req models.Request) {
idOrName, ok := req.Params["name"].(string)
idOrName, ok := models.Get[string](req, "name")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
return

View File

@@ -45,7 +45,7 @@ func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
func handleSetTemperature(conn net.Conn, req models.Request, manager *Manager) {
var lowTemp, highTemp int
if temp, ok := req.Params["temp"].(float64); ok {
if temp, ok := models.Get[float64](req, "temp"); ok {
lowTemp = int(temp)
highTemp = int(temp)
} else {
@@ -93,24 +93,10 @@ func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) {
}
func handleSetManualTimes(conn net.Conn, req models.Request, manager *Manager) {
sunriseParam := req.Params["sunrise"]
sunsetParam := req.Params["sunset"]
sunriseStr, sunriseOK := models.Get[string](req, "sunrise")
sunsetStr, sunsetOK := models.Get[string](req, "sunset")
if sunriseParam == nil || sunsetParam == nil {
manager.ClearManualTimes()
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "manual times cleared"})
return
}
sunriseStr, ok := sunriseParam.(string)
if !ok || sunriseStr == "" {
manager.ClearManualTimes()
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "manual times cleared"})
return
}
sunsetStr, ok := sunsetParam.(string)
if !ok || sunsetStr == "" {
if !sunriseOK || !sunsetOK || sunriseStr == "" || sunsetStr == "" {
manager.ClearManualTimes()
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "manual times cleared"})
return

View File

@@ -56,7 +56,7 @@ func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
}
func handleApplyConfiguration(conn net.Conn, req models.Request, manager *Manager, test bool) {
headsParam, ok := req.Params["heads"]
headsParam, ok := models.Get[any](req, "heads")
if !ok {
models.RespondError(conn, req.ID, "missing 'heads' parameter")
return