// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later package tentacle import "git.stormux.org/storm/cthulhu/internal/input" // Binding proposes a default. It does not acquire a native grab. Scope follows // the existing global/browse rule: neither may shadow the other in one layout. type Binding struct { Layout string `json:"layout"` Scope string `json:"scope"` Key string `json:"key"` } type BindingStatus struct { Command string Suggested Binding Bound bool Conflict string // Existing command ID; empty when there is no conflict. } func binding_layout(name string) input.KeyboardLayout { if name == "desktop" { return input.DesktopKeyboardLayout } return input.LaptopKeyboardLayout } func validate_binding(binding Binding) error { if (binding.Layout != "desktop" && binding.Layout != "laptop") || (binding.Scope != "global" && binding.Scope != "browse") || len(binding.Key) > 128 { return ErrProtocol } if _, err := input.ParseCommandChord(binding.Key); err != nil { return ErrProtocol } return nil } func (session *Session) Bindings() []BindingStatus { session.mutex.Lock() defer session.mutex.Unlock() return append([]BindingStatus(nil), session.bindings...) } // Caller holds manager.mutex, but no session lock. Revoked sessions immediately // stop reserving chords even while their processes are still shutting down. func (manager *Manager) occupied_bindings() map[input.KeyboardLayout]input.CommandBindings { occupied := make(map[input.KeyboardLayout]input.CommandBindings) for layout, bindings := range manager.coreBindings { occupied[layout] = make(input.CommandBindings) for chord, command := range bindings { occupied[layout][chord] = command } } for _, session := range manager.sessions { session.mutex.Lock() if session.state == Active { for _, binding := range session.bindings { if binding.Bound { chord, _ := input.ParseCommandChord(binding.Suggested.Key) occupied[binding_layout(binding.Suggested.Layout)][chord] = input.Command(binding.Command) } } } session.mutex.Unlock() } return occupied } func resolve_bindings(identity string, commands []Command, occupied map[input.KeyboardLayout]input.CommandBindings) []BindingStatus { var result []BindingStatus for _, command := range commands { commandID := "tentacle/" + identity + "/" + command.ID for _, binding := range command.Bindings { chord, _ := input.ParseCommandChord(binding.Key) bindings := occupied[binding_layout(binding.Layout)] previous := bindings[chord] result = append(result, BindingStatus{Command: commandID, Suggested: binding, Bound: previous == "", Conflict: string(previous)}) if previous == "" { bindings[chord] = input.Command(commandID) } } } return result }