Big Refactor (#2)

Refactor Everything.

Co-authored-by: Anna Rose Wiggins <annabunches@gmail.com>
Co-committed-by: Anna Rose Wiggins <annabunches@gmail.com>
This commit is contained in:
Anna Rose Wiggins 2025-07-08 03:26:24 +00:00 committed by Anna Rose
parent a0949e719f
commit ff38db6596
21 changed files with 413 additions and 309 deletions

View file

@ -75,7 +75,7 @@ func main() {
timerCount := 0 timerCount := 0
for _, rule := range rules { for _, rule := range rules {
if timedRule, ok := rule.(*mappingrules.MappingRuleProportionalAxis); ok { if timedRule, ok := rule.(*mappingrules.MappingRuleAxisToButton); ok {
go timerWatcher(timedRule, eventChannel) go timerWatcher(timedRule, eventChannel)
timerCount++ timerCount++
} }

View file

@ -28,7 +28,7 @@ func eventWatcher(device *evdev.InputDevice, channel chan<- ChannelEvent) {
} }
} }
func timerWatcher(rule *mappingrules.MappingRuleProportionalAxis, channel chan<- ChannelEvent) { func timerWatcher(rule *mappingrules.MappingRuleAxisToButton, channel chan<- ChannelEvent) {
for { for {
event := rule.TimerEvent() event := rule.TimerEvent()
if event != nil { if event != nil {

View file

@ -0,0 +1,56 @@
package config
import (
"errors"
"fmt"
"git.annabunches.net/annabunches/joyful/internal/mappingrules"
"github.com/holoplot/go-evdev"
)
func makeRuleTargetButton(targetConfig RuleTargetConfig, devs map[string]*evdev.InputDevice) (*mappingrules.RuleTargetButton, error) {
device, ok := devs[targetConfig.Device]
if !ok {
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
}
eventCode, ok := evdev.KEYFromString[targetConfig.Button]
if !ok {
return nil, fmt.Errorf("invalid button code '%s'", targetConfig.Button)
}
return mappingrules.NewRuleTargetButton(
targetConfig.Device,
device,
eventCode,
targetConfig.Inverted,
), nil
}
func makeRuleTargetAxis(targetConfig RuleTargetConfig, devs map[string]*evdev.InputDevice) (*mappingrules.RuleTargetAxis, error) {
device, ok := devs[targetConfig.Device]
if !ok {
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
}
eventCode, ok := evdev.ABSFromString[targetConfig.Axis]
if !ok {
return nil, fmt.Errorf("invalid button code '%s'", targetConfig.Button)
}
return mappingrules.NewRuleTargetAxis(
targetConfig.Device,
device,
eventCode,
targetConfig.Inverted,
0, 0, 0, // TODO: replace these with real values
), nil
}
func makeRuleTargetModeSelect(targetConfig RuleTargetConfig, allModes []string) (*mappingrules.RuleTargetModeSelect, error) {
if ok := validateModes(targetConfig.Modes, allModes); !ok {
return nil, errors.New("undefined mode in mode select list")
}
return mappingrules.NewRuleTargetModeSelect(targetConfig.Modes)
}

View file

@ -1,8 +1,8 @@
package config package config
import ( import (
"errors"
"fmt" "fmt"
"slices"
"strings" "strings"
"git.annabunches.net/annabunches/joyful/internal/logger" "git.annabunches.net/annabunches/joyful/internal/logger"
@ -23,23 +23,32 @@ func (parser *ConfigParser) BuildRules(pDevs map[string]*evdev.InputDevice, vDev
var newRule mappingrules.MappingRule var newRule mappingrules.MappingRule
var err error var err error
baseParams, err := setBaseRuleParameters(ruleConfig, modes) if ok := validateModes(ruleConfig.Modes, modes); !ok {
if err != nil { logger.Logf("Skipping rule '%s', mode list specifies undefined mode.", ruleConfig.Name)
logger.LogErrorf(err, "couldn't set output parameters, skipping rule '%s'", ruleConfig.Name)
continue continue
} }
base := mappingrules.NewMappingRuleBase(ruleConfig.Name, ruleConfig.Modes)
switch strings.ToLower(ruleConfig.Type) { switch strings.ToLower(ruleConfig.Type) {
case RuleTypeSimple: case RuleTypeButton:
newRule, err = makeMappingRuleButton(ruleConfig, pDevs, vDevs, baseParams) newRule, err = makeMappingRuleButton(ruleConfig, pDevs, vDevs, base)
case RuleTypeCombo: case RuleTypeButtonCombo:
newRule, err = makeComboRule(ruleConfig, pDevs, vDevs, baseParams) newRule, err = makeMappingRuleCombo(ruleConfig, pDevs, vDevs, base)
case RuleTypeLatched: case RuleTypeLatched:
newRule, err = makeLatchedRule(ruleConfig, pDevs, vDevs, baseParams) newRule, err = makeMappingRuleLatched(ruleConfig, pDevs, vDevs, base)
case RuleTypeAxis:
newRule, err = makeMappingRuleAxis(ruleConfig, pDevs, vDevs, base)
case RuleTypeAxisToButton:
newRule, err = makeMappingRuleAxisToButton(ruleConfig, pDevs, vDevs, base)
case RuleTypeModeSelect:
newRule, err = makeMappingRuleModeSelect(ruleConfig, pDevs, modes, base)
default:
err = fmt.Errorf("bad rule type '%s' for rule '%s'", ruleConfig.Type, ruleConfig.Name)
} }
if err != nil { if err != nil {
logger.LogError(err, "failed to build rule") logger.LogErrorf(err, "Failed to build rule '%s'", ruleConfig.Name)
continue continue
} }
@ -49,56 +58,28 @@ func (parser *ConfigParser) BuildRules(pDevs map[string]*evdev.InputDevice, vDev
return rules return rules
} }
func setBaseRuleParameters(ruleConfig RuleConfig, modes []string) (mappingrules.MappingRuleBase, error) {
// We perform this check here instead of in makeRuleTarget because only Output targets
// can meaningfully have ModeSelect; this lets us avoid plumbing the modes in on every
// makeRuleTarget call.
if len(ruleConfig.Output.ModeSelect) > 0 {
err := validateModes(ruleConfig.Output.ModeSelect, modes)
if err != nil {
return mappingrules.MappingRuleBase{}, err
}
}
err := validateModes(ruleConfig.Modes, modes)
if err != nil {
return mappingrules.MappingRuleBase{}, err
}
ruleModes := ensureModes(ruleConfig.Modes)
return mappingrules.MappingRuleBase{
Modes: ruleModes,
Name: ruleConfig.Name,
}, nil
}
func makeMappingRuleButton(ruleConfig RuleConfig, func makeMappingRuleButton(ruleConfig RuleConfig,
pDevs map[string]*evdev.InputDevice, pDevs map[string]*evdev.InputDevice,
vDevs map[string]*evdev.InputDevice, vDevs map[string]*evdev.InputDevice,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButton, error) { base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButton, error) {
input, err := makeRuleTarget(ruleConfig.Input, pDevs) input, err := makeRuleTargetButton(ruleConfig.Input, pDevs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
output, err := makeRuleTarget(ruleConfig.Output, vDevs) output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &mappingrules.MappingRuleButton{ return mappingrules.NewMappingRuleButton(base, input, output), nil
MappingRuleBase: base,
Input: input.(*mappingrules.RuleTargetButton),
Output: output.(*mappingrules.RuleTargetButton),
}, nil
} }
func makeComboRule(ruleConfig RuleConfig, func makeMappingRuleCombo(ruleConfig RuleConfig,
pDevs map[string]*evdev.InputDevice, pDevs map[string]*evdev.InputDevice,
vDevs map[string]*evdev.InputDevice, vDevs map[string]*evdev.InputDevice,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleCombo, error) { base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonCombo, error) {
inputs := make([]*mappingrules.RuleTargetButton, 0) inputs := make([]*mappingrules.RuleTargetButton, 0)
for _, inputConfig := range ruleConfig.Inputs { for _, inputConfig := range ruleConfig.Inputs {
@ -109,142 +90,73 @@ func makeComboRule(ruleConfig RuleConfig,
inputs = append(inputs, input) inputs = append(inputs, input)
} }
return &mappingrules.MappingRuleCombo{ output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
MappingRuleBase: base, if err != nil {
Inputs: inputs, return nil, err
State: 0, }
}, nil
return mappingrules.NewMappingRuleButtonCombo(base, inputs, output), nil
} }
func makeLatchedRule(ruleConfig RuleConfig, func makeMappingRuleLatched(ruleConfig RuleConfig,
pDevs map[string]*evdev.InputDevice, pDevs map[string]*evdev.InputDevice,
vDevs map[string]*evdev.InputDevice, vDevs map[string]*evdev.InputDevice,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleLatched, error) { base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonLatched, error) {
input, err := makeRuleTargetButton(ruleConfig.Input, pDevs) input, err := makeRuleTargetButton(ruleConfig.Input, pDevs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &mappingrules.MappingRuleLatched{ output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
MappingRuleBase: base,
Input: input,
State: false,
}, nil
}
func makeRuleTargetButton(targetConfig RuleTargetConfig, devs map[string]*evdev.InputDevice) (*mappingrules.RuleTargetButton, error) {
device, ok := devs[targetConfig.Device]
if !ok {
return nil, fmt.Errorf("couldn't build rule due to non-existent device '%s'", targetConfig.Device)
}
_, eventCode, err := decodeRuleTargetValues(targetConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
baseParams := mappingrules.RuleTargetBase{ return mappingrules.NewMappingRuleButtonLatched(base, input, output), nil
DeviceName: targetConfig.Device,
Device: device,
Inverted: targetConfig.Inverted,
Code: eventCode,
}
return &mappingrules.RuleTargetButton{
RuleTargetBase: baseParams,
}, nil
} }
// makeRuleTarget takes an Input declaration from the YAML and returns a fully formed RuleTarget. func makeMappingRuleAxis(ruleConfig RuleConfig,
func makeRuleTarget(targetConfig RuleTargetConfig, devs map[string]*evdev.InputDevice) (mappingrules.RuleTarget, error) { pDevs map[string]*evdev.InputDevice,
if len(targetConfig.ModeSelect) > 0 { vDevs map[string]*evdev.InputDevice,
return &mappingrules.RuleTargetModeSelect{ base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxis, error) {
ModeSelect: targetConfig.ModeSelect,
}, nil
}
device, ok := devs[targetConfig.Device] input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs)
if !ok {
return nil, fmt.Errorf("couldn't build rule due to non-existent device '%s'", targetConfig.Device)
}
eventType, eventCode, err := decodeRuleTargetValues(targetConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
baseParams := mappingrules.RuleTargetBase{ output, err := makeRuleTargetAxis(ruleConfig.Output, vDevs)
DeviceName: targetConfig.Device, if err != nil {
Device: device, return nil, err
Inverted: targetConfig.Inverted,
Code: eventCode,
} }
switch eventType { return mappingrules.NewMappingRuleAxis(base, input, output), nil
case evdev.EV_KEY:
return &mappingrules.RuleTargetButton{
RuleTargetBase: baseParams,
}, nil
case evdev.EV_ABS:
return &mappingrules.RuleTargetAxis{
RuleTargetBase: baseParams,
AxisStart: targetConfig.AxisStart,
AxisEnd: targetConfig.AxisEnd,
}, nil
default:
return nil, fmt.Errorf("skipping rule due to unsupported event type '%d'", eventType)
}
} }
// decodeRuleTargetValues returns the appropriate evdev.EvType and evdev.EvCode values // STUB
// for a given RuleTargetConfig, converting the config file strings into appropriate constants func makeMappingRuleAxisToButton(ruleConfig RuleConfig,
// pDevs map[string]*evdev.InputDevice,
// Todo: support different formats for key specification vDevs map[string]*evdev.InputDevice,
func decodeRuleTargetValues(target RuleTargetConfig) (evdev.EvType, evdev.EvCode, error) { base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisToButton, error) {
var eventType evdev.EvType
var eventCode evdev.EvCode
var ok bool
if target.Button != "" { return nil, errors.New("stub: makeMappingRuleAxisToButton")
eventType = evdev.EV_KEY
eventCode, ok = evdev.KEYFromString[target.Button]
if !ok {
return 0, 0, fmt.Errorf("skipping rule due to invalid button code '%s'", target.Button)
}
} else if target.Axis != "" {
eventType = evdev.EV_ABS
eventCode, ok = evdev.ABSFromString[target.Axis]
if !ok {
return 0, 0, fmt.Errorf("skipping rule due to invalid axis code '%s'", target.Button)
}
}
return eventType, eventCode, nil
} }
// ensureModes either returns the mode list, or if it is empty returns []string{"*"} func makeMappingRuleModeSelect(ruleConfig RuleConfig,
func ensureModes(modes []string) []string { pDevs map[string]*evdev.InputDevice,
if len(modes) == 0 { modes []string,
return []string{"*"} base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleModeSelect, error) {
input, err := makeRuleTargetButton(ruleConfig.Input, pDevs)
if err != nil {
return nil, err
} }
return modes
} output, err := makeRuleTargetModeSelect(ruleConfig.Output, modes)
if err != nil {
// validateModes checks the provided modes against a larger subset of modes (usually all defined ones) return nil, err
// and returns an error if any of the modes are not defined. }
func validateModes(modes []string, allModes []string) error {
if len(modes) == 0 { return mappingrules.NewMappingRuleModeSelect(base, input, output), nil
return nil
}
for _, mode := range modes {
if !slices.Contains(allModes, mode) {
return fmt.Errorf("mode list specifies undefined mode '%s'", mode)
}
}
return nil
} }

19
internal/config/modes.go Normal file
View file

@ -0,0 +1,19 @@
package config
import "slices"
// validateModes checks the provided modes against a larger subset of modes (usually all defined ones)
// and returns false if any of the modes are not defined.
func validateModes(modes []string, allModes []string) bool {
if len(modes) == 0 {
return true
}
for _, mode := range modes {
if !slices.Contains(allModes, mode) {
return false
}
}
return true
}

View file

@ -28,11 +28,11 @@ type RuleConfig struct {
} }
type RuleTargetConfig struct { type RuleTargetConfig struct {
Device string `yaml:"device,omitempty"` Device string `yaml:"device,omitempty"`
Button string `yaml:"button,omitempty"` Button string `yaml:"button,omitempty"`
Axis string `yaml:"axis,omitempty"` Axis string `yaml:"axis,omitempty"`
AxisStart int32 `yaml:"axis_start,omitempty"` DeadzoneStart int32 `yaml:"axis_start,omitempty"`
AxisEnd int32 `yaml:"axis_end,omitempty"` DeadzoneEnd int32 `yaml:"axis_end,omitempty"`
Inverted bool `yaml:"inverted,omitempty"` Inverted bool `yaml:"inverted,omitempty"`
ModeSelect []string `yaml:"mode_select,omitempty"` Modes []string `yaml:"modes,omitempty"`
} }

View file

@ -4,6 +4,18 @@ import (
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
) )
const (
DeviceTypePhysical = "physical"
DeviceTypeVirtual = "virtual"
RuleTypeButton = "button"
RuleTypeButtonCombo = "button-combo"
RuleTypeLatched = "button-latched"
RuleTypeAxis = "axis"
RuleTypeModeSelect = "mode-select"
RuleTypeAxisToButton = "axis-to-button"
)
var ( var (
ButtonFromIndex = []evdev.EvCode{ ButtonFromIndex = []evdev.EvCode{
evdev.BTN_TRIGGER, evdev.BTN_TRIGGER,
@ -60,12 +72,3 @@ var (
evdev.BTN_TRIGGER_HAPPY40, evdev.BTN_TRIGGER_HAPPY40,
} }
) )
const (
DeviceTypePhysical = "physical"
DeviceTypeVirtual = "virtual"
RuleTypeSimple = "simple"
RuleTypeCombo = "combo"
RuleTypeLatched = "latched"
)

View file

@ -25,7 +25,7 @@ func LogErrorf(err error, msg string, params ...interface{}) {
if msg == "" { if msg == "" {
fmt.Printf("%s\n", err.Error()) fmt.Printf("%s\n", err.Error())
} else { } else {
fmt.Printf("%s: %s\n", err.Error(), fmt.Sprintf(msg, params...)) fmt.Printf("%s: %s\n", fmt.Sprintf(msg, params...), err.Error())
} }
} }

View file

@ -13,11 +13,16 @@ type RuleTarget interface {
// (e.g., inverting the value if Inverted == true) // (e.g., inverting the value if Inverted == true)
NormalizeValue(int32) int32 NormalizeValue(int32) int32
// CreateEvent typically takes the (probably normalized) value and returns an event that can be emitted // MatchEvent returns true if the provided device and input event are a match for this rule target
// on a virtual device. ValidateEvent(*evdev.InputDevice, *evdev.InputEvent) bool
//
// CreateEvent creates an event that can be emitted on a virtual device.
// For RuleTargetModeSelect, this method modifies the active mode and returns nil. // For RuleTargetModeSelect, this method modifies the active mode and returns nil.
// //
// TODO: should we normalize inside this function to simplify the interface? // CreateEvent does not do any error checking; it assumes it is receiving verified and sanitized output.
// The caller is responsible for determining whether an event *should* be emitted.
//
// Typically int32 is the input event's normalized value. *string is the current mode, but is optional
// for most implementations.
CreateEvent(int32, *string) *evdev.InputEvent CreateEvent(int32, *string) *evdev.InputEvent
} }

View file

@ -0,0 +1,27 @@
package mappingrules
import "github.com/holoplot/go-evdev"
// A Simple Mapping Rule can map a button to a button or an axis to an axis.
type MappingRuleAxis struct {
MappingRuleBase
Input *RuleTargetAxis
Output *RuleTargetAxis
}
func NewMappingRuleAxis(base MappingRuleBase, input *RuleTargetAxis, output *RuleTargetAxis) *MappingRuleAxis {
return &MappingRuleAxis{
MappingRuleBase: base,
Input: input,
Output: output,
}
}
func (rule *MappingRuleAxis) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
if !rule.MappingRuleBase.modeCheck(mode) ||
!rule.Input.MatchEvent(device, event) {
return nil, nil
}
return rule.Output.Device, rule.Output.CreateEvent(rule.Input.NormalizeValue(event.Value), mode)
}

View file

@ -0,0 +1,51 @@
package mappingrules
import (
"time"
"github.com/holoplot/go-evdev"
)
// TODO: This whole file is still WIP
type MappingRuleAxisToButton struct {
MappingRuleBase
Input *RuleTargetAxis
Output *RuleTargetButton
RepeatSpeedMin int32
RepeatSpeedMax int32
lastValue int32
lastEvent time.Time
}
func (rule *MappingRuleAxisToButton) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
if !rule.MappingRuleBase.modeCheck(mode) ||
!rule.Input.MatchEvent(device, event) {
return nil, nil
}
// set the last value to the normalized input value
rule.lastValue = rule.Input.NormalizeValue(event.Value)
return nil, nil
}
// TimerEvent returns an event when enough time has passed (compared to the last recorded axis value)
// to emit an event.
func (rule *MappingRuleAxisToButton) TimerEvent() *evdev.InputEvent {
// This is tighter coupling than we'd like, but it will do for now.
// TODO: maybe it would be better to just be more declarative about event types and their inputs and outputs.
if rule.lastValue < rule.Input.DeadzoneStart {
rule.lastEvent = time.Now()
return nil
}
// calculate target time until next event press
// nextEvent := rule.LastEvent + (rule.LastValue)
// TODO: figure out what the condition should be
if false {
// TODO: emit event
rule.lastEvent = time.Now()
}
return nil
}

View file

@ -7,6 +7,20 @@ type MappingRuleBase struct {
Modes []string Modes []string
} }
func NewMappingRuleBase(
name string,
modes []string,
) MappingRuleBase {
if len(modes) == 0 {
modes = []string{"*"}
}
return MappingRuleBase{
Name: name,
Modes: modes,
}
}
func (rule *MappingRuleBase) modeCheck(mode *string) bool { func (rule *MappingRuleBase) modeCheck(mode *string) bool {
if rule.Modes[0] == "*" { if rule.Modes[0] == "*" {
return true return true

View file

@ -9,13 +9,25 @@ type MappingRuleButton struct {
Output *RuleTargetButton Output *RuleTargetButton
} }
func NewMappingRuleButton(
base MappingRuleBase,
input *RuleTargetButton,
output *RuleTargetButton) *MappingRuleButton {
return &MappingRuleButton{
MappingRuleBase: base,
Input: input,
Output: output,
}
}
func (rule *MappingRuleButton) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { func (rule *MappingRuleButton) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
if !rule.MappingRuleBase.modeCheck(mode) { if !rule.MappingRuleBase.modeCheck(mode) {
return nil, nil return nil, nil
} }
if device != rule.Input.GetDevice() || if device != rule.Input.Device ||
event.Code != rule.Input.GetCode() { event.Code != rule.Input.Button {
return nil, nil return nil, nil
} }

View file

@ -3,24 +3,37 @@ package mappingrules
import "github.com/holoplot/go-evdev" import "github.com/holoplot/go-evdev"
// A Combo Mapping Rule can require multiple physical button presses for a single output button // A Combo Mapping Rule can require multiple physical button presses for a single output button
type MappingRuleCombo struct { type MappingRuleButtonCombo struct {
MappingRuleBase MappingRuleBase
Inputs []*RuleTargetButton Inputs []*RuleTargetButton
Output *RuleTargetButton Output *RuleTargetButton
State int State int
} }
func (rule *MappingRuleCombo) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { func NewMappingRuleButtonCombo(
base MappingRuleBase,
inputs []*RuleTargetButton,
output *RuleTargetButton) *MappingRuleButtonCombo {
return &MappingRuleButtonCombo{
MappingRuleBase: base,
Inputs: inputs,
Output: output,
State: 0,
}
}
func (rule *MappingRuleButtonCombo) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
if !rule.MappingRuleBase.modeCheck(mode) { if !rule.MappingRuleBase.modeCheck(mode) {
return nil, nil return nil, nil
} }
// Check each of the inputs, and if we find a match, proceed // Check each of the inputs, and if we find a match, proceed
var match RuleTarget var match *RuleTargetButton
for _, input := range rule.Inputs { for _, input := range rule.Inputs {
if device == input.GetDevice() && if input.MatchEvent(device, event) {
event.Code == input.GetCode() {
match = input match = input
break
} }
} }
@ -40,10 +53,10 @@ func (rule *MappingRuleCombo) MatchEvent(device *evdev.InputDevice, event *evdev
targetState := len(rule.Inputs) targetState := len(rule.Inputs)
if oldState == targetState-1 && rule.State == targetState { if oldState == targetState-1 && rule.State == targetState {
return rule.Output.GetDevice(), rule.Output.CreateEvent(1, mode) return rule.Output.Device, rule.Output.CreateEvent(1, mode)
} }
if oldState == targetState && rule.State == targetState-1 { if oldState == targetState && rule.State == targetState-1 {
return rule.Output.GetDevice(), rule.Output.CreateEvent(0, mode) return rule.Output.Device, rule.Output.CreateEvent(0, mode)
} }
return nil, nil return nil, nil
} }

View file

@ -2,20 +2,33 @@ package mappingrules
import "github.com/holoplot/go-evdev" import "github.com/holoplot/go-evdev"
type MappingRuleLatched struct { type MappingRuleButtonLatched struct {
MappingRuleBase MappingRuleBase
Input *RuleTargetButton Input *RuleTargetButton
Output *RuleTargetButton Output *RuleTargetButton
State bool State bool
} }
func (rule *MappingRuleLatched) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { func NewMappingRuleButtonLatched(
base MappingRuleBase,
input *RuleTargetButton,
output *RuleTargetButton) *MappingRuleButtonLatched {
return &MappingRuleButtonLatched{
MappingRuleBase: base,
Input: input,
Output: output,
State: false,
}
}
func (rule *MappingRuleButtonLatched) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
if !rule.MappingRuleBase.modeCheck(mode) { if !rule.MappingRuleBase.modeCheck(mode) {
return nil, nil return nil, nil
} }
if device != rule.Input.Device || if device != rule.Input.Device ||
event.Code != rule.Input.Code || event.Code != rule.Input.Button ||
rule.Input.NormalizeValue(event.Value) == 0 { rule.Input.NormalizeValue(event.Value) == 0 {
return nil, nil return nil, nil
} }

View file

@ -0,0 +1,40 @@
package mappingrules
import "github.com/holoplot/go-evdev"
type MappingRuleModeSelect struct {
MappingRuleBase
Input *RuleTargetButton
Output *RuleTargetModeSelect
}
func NewMappingRuleModeSelect(
base MappingRuleBase,
input *RuleTargetButton,
output *RuleTargetModeSelect,
) *MappingRuleModeSelect {
return &MappingRuleModeSelect{
MappingRuleBase: base,
Input: input,
Output: output,
}
}
func (rule *MappingRuleModeSelect) MatchEvent(
device *evdev.InputDevice,
event *evdev.InputEvent,
mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
if !rule.MappingRuleBase.modeCheck(mode) {
return nil, nil
}
if device != rule.Input.Device ||
event.Code != rule.Input.Button ||
rule.Input.NormalizeValue(event.Value) == 0 {
return nil, nil
}
return nil, rule.Output.CreateEvent(event.Value, mode)
}

View file

@ -1,54 +0,0 @@
package mappingrules
import (
"time"
"github.com/holoplot/go-evdev"
)
// TODO: How are we going to implement this? It needs to operate on a timer...
type MappingRuleProportionalAxis struct {
MappingRuleBase
Input *RuleTargetAxis
Output *RuleTargetButton
Sensitivity int32
LastValue int32
LastEvent time.Time
}
func (rule *MappingRuleProportionalAxis) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
if !rule.MappingRuleBase.modeCheck(mode) {
return nil, nil
}
if device != rule.Input.GetDevice() ||
event.Code != rule.Input.GetCode() {
return nil, nil
}
// set the last value to the normalized input value
rule.LastValue = rule.Input.NormalizeValue(event.Value)
return nil, nil
}
// TimerEvent returns an event when enough time has passed (compared to the last recorded axis value)
// to emit an event.
func (rule *MappingRuleProportionalAxis) TimerEvent() *evdev.InputEvent {
// This is tighter coupling than we'd like, but it will do for now.
// TODO: maybe it would be better to just be more declarative about event types and their inputs and outputs.
if rule.LastValue < rule.Input.AxisStart {
rule.LastEvent = time.Now()
return nil
}
// calculate target time until next event press
// nextEvent := rule.LastEvent + (rule.LastValue)
// TODO: figure out what the condition should be
if false {
// TODO: emit event
rule.LastEvent = time.Now()
}
return nil
}

View file

@ -5,35 +5,43 @@ import (
) )
type RuleTargetAxis struct { type RuleTargetAxis struct {
RuleTargetBase DeviceName string
AxisStart int32 Device *evdev.InputDevice
AxisEnd int32 Axis evdev.EvCode
Sensitivity float64 Inverted bool
DeadzoneStart int32
DeadzoneEnd int32
Sensitivity float64
} }
func NewRuleTargetAxis(device_name string, func NewRuleTargetAxis(device_name string,
device *evdev.InputDevice, device *evdev.InputDevice,
code evdev.EvCode, axis evdev.EvCode,
inverted bool, inverted bool,
axis_start int32, deadzone_start int32,
axis_end int32, deadzone_end int32,
sensitivity float64) *RuleTargetAxis { sensitivity float64) *RuleTargetAxis {
return &RuleTargetAxis{ return &RuleTargetAxis{
RuleTargetBase: NewRuleTargetBase(device_name, device, code, inverted), DeviceName: device_name,
AxisStart: axis_start, Device: device,
AxisEnd: axis_end, Axis: axis,
Sensitivity: sensitivity, Inverted: inverted,
DeadzoneStart: deadzone_start,
DeadzoneEnd: deadzone_end,
Sensitivity: sensitivity,
} }
} }
// TODO: lots of fixes and decisions to make here. Should we normalize all axes to the same range?
// How do we handle deadzones in light of that?
func (target *RuleTargetAxis) NormalizeValue(value int32) int32 { func (target *RuleTargetAxis) NormalizeValue(value int32) int32 {
if !target.Inverted { if !target.Inverted {
return value return value
} }
axisRange := target.AxisEnd - target.AxisStart axisRange := target.DeadzoneEnd - target.DeadzoneStart
axisMid := target.AxisEnd - axisRange/2 axisMid := target.DeadzoneEnd - axisRange/2
delta := value - axisMid delta := value - axisMid
if delta < 0 { if delta < 0 {
delta = -delta delta = -delta
@ -55,7 +63,13 @@ func (target *RuleTargetAxis) CreateEvent(value int32, mode *string) *evdev.Inpu
// TODO: oh no we need center deadzones actually... // TODO: oh no we need center deadzones actually...
return &evdev.InputEvent{ return &evdev.InputEvent{
Type: evdev.EV_ABS, Type: evdev.EV_ABS,
Code: target.Code, Code: target.Axis,
Value: value, Value: value,
} }
} }
func (target *RuleTargetAxis) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent) bool {
return device == target.Device &&
event.Type == evdev.EV_ABS &&
event.Code == target.Axis
}

View file

@ -1,35 +0,0 @@
package mappingrules
import "github.com/holoplot/go-evdev"
type RuleTargetBase struct {
DeviceName string
Device *evdev.InputDevice
Code evdev.EvCode
Inverted bool
}
func NewRuleTargetBase(device_name string,
device *evdev.InputDevice,
code evdev.EvCode,
inverted bool) RuleTargetBase {
return RuleTargetBase{
DeviceName: device_name,
Device: device,
Code: code,
Inverted: inverted,
}
}
func (target *RuleTargetBase) GetCode() evdev.EvCode {
return target.Code
}
func (target *RuleTargetBase) GetDeviceName() string {
return target.DeviceName
}
func (target *RuleTargetBase) GetDevice() *evdev.InputDevice {
return target.Device
}

View file

@ -3,12 +3,18 @@ package mappingrules
import "github.com/holoplot/go-evdev" import "github.com/holoplot/go-evdev"
type RuleTargetButton struct { type RuleTargetButton struct {
RuleTargetBase DeviceName string
Device *evdev.InputDevice
Button evdev.EvCode
Inverted bool
} }
func NewRuleTargetButton(device_name string, device *evdev.InputDevice, code evdev.EvCode, inverted bool) *RuleTargetButton { func NewRuleTargetButton(device_name string, device *evdev.InputDevice, code evdev.EvCode, inverted bool) *RuleTargetButton {
return &RuleTargetButton{ return &RuleTargetButton{
RuleTargetBase: NewRuleTargetBase(device_name, device, code, inverted), DeviceName: device_name,
Device: device,
Button: code,
Inverted: inverted,
} }
} }
@ -22,10 +28,16 @@ func (target *RuleTargetButton) NormalizeValue(value int32) int32 {
return value return value
} }
func (target *RuleTargetButton) CreateEvent(value int32, mode *string) *evdev.InputEvent { func (target *RuleTargetButton) CreateEvent(value int32, _ *string) *evdev.InputEvent {
return &evdev.InputEvent{ return &evdev.InputEvent{
Type: evdev.EV_KEY, Type: evdev.EV_KEY,
Code: target.Code, Code: target.Button,
Value: value, Value: value,
} }
} }
func (target *RuleTargetButton) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent) bool {
return device == target.Device &&
event.Type == evdev.EV_KEY &&
event.Code == target.Button
}

View file

@ -1,6 +1,7 @@
package mappingrules package mappingrules
import ( import (
"errors"
"slices" "slices"
"git.annabunches.net/annabunches/joyful/internal/logger" "git.annabunches.net/annabunches/joyful/internal/logger"
@ -8,34 +9,35 @@ import (
) )
type RuleTargetModeSelect struct { type RuleTargetModeSelect struct {
RuleTargetBase Modes []string
ModeSelect []string
} }
func NewRuleTargetModeSelect(modes []string) *RuleTargetModeSelect { func NewRuleTargetModeSelect(modes []string) (*RuleTargetModeSelect, error) {
return &RuleTargetModeSelect{ if len(modes) == 0 {
RuleTargetBase: NewRuleTargetBase("", nil, 0, false), return nil, errors.New("cannot create RuleTargetModeSelect: mode list is empty")
ModeSelect: modes,
} }
return &RuleTargetModeSelect{
Modes: modes,
}, nil
} }
// RuleTargetModeSelect doesn't make sense as an input type // RuleTargetModeSelect doesn't make sense as an input type
func (target *RuleTargetModeSelect) NormalizeValue(value int32) int32 { func (target *RuleTargetModeSelect) NormalizeValue(_ int32) int32 {
return -1 return -1
} }
func (target *RuleTargetModeSelect) CreateEvent(value int32, mode *string) *evdev.InputEvent { func (target *RuleTargetModeSelect) CreateEvent(_ int32, mode *string) *evdev.InputEvent {
if value == 0 {
return nil
}
index := 0 index := 0
if currentMode := slices.Index(target.ModeSelect, *mode); currentMode != -1 { if currentMode := slices.Index(target.Modes, *mode); currentMode != -1 {
// find the next mode // find the next mode
index = (currentMode + 1) % len(target.ModeSelect) index = (currentMode + 1) % len(target.Modes)
} }
*mode = target.ModeSelect[index] *mode = target.Modes[index]
logger.Logf("Mode changed to '%s'", *mode) logger.Logf("Mode changed to '%s'", *mode)
return nil return nil
} }
func (target *RuleTargetModeSelect) MatchEvent(_ *evdev.InputDevice, _ *evdev.InputEvent) bool {
return false
}