classify_bindings.go
package calls
import (
"slices"
"sort"
"skraak/datafile"
)
// KeyBinding maps a key to a species/calltype for TUI classification.
type KeyBinding struct {
Key string // single char: "k", "n", "p"
Species string // "Kiwi", "Don't Know", "Morepork"
CallType string // "Duet", "Female", "Male" (optional)
}
// BindingResult represents parsed key result for TUI classification.
type BindingResult struct {
Species string
CallType string // empty string = remove calltype
}
// ParseKeyBuffer parses a single key into binding result.
// Returns nil if no matching binding is found.
func (s *ClassifyState) ParseKeyBuffer(key string) *BindingResult {
for _, b := range s.Config.Bindings {
if b.Key == key {
return &BindingResult{
Species: b.Species,
CallType: b.CallType,
}
}
}
return nil
}
// ApplyBinding applies a binding result to the current segment.
// This is a TUI operation that modifies the segment's labels.
func (s *ClassifyState) ApplyBinding(result *BindingResult) {
seg := s.CurrentSegment()
if seg == nil {
return
}
df := s.CurrentFile()
if df == nil {
return
}
// Set reviewer
df.Meta.Reviewer = s.Config.Reviewer
// Get labels matching filter
filterLabels := seg.GetFilterLabels(s.Config.Filter)
// Determine certainty: 0 for Don't Know, 100 for others
certainty := 100
if result.Species == "Don't Know" {
certainty = 0
}
if len(filterLabels) == 0 {
// No matching labels, add new one
seg.Labels = append(seg.Labels, &datafile.Label{
Species: result.Species,
Certainty: certainty,
Filter: s.Config.Filter,
CallType: result.CallType,
})
} else {
// Edit first matching label, remove rest
filterLabels[0].Species = result.Species
filterLabels[0].Certainty = certainty
filterLabels[0].CallType = result.CallType // always set (empty = remove)
// Remove extra matching labels
if len(filterLabels) > 1 {
var newLabels []*datafile.Label
for _, l := range seg.Labels {
keep := !slices.Contains(filterLabels[1:], l)
if keep {
newLabels = append(newLabels, l)
}
}
seg.Labels = newLabels
}
}
// Re-sort labels
sort.Slice(seg.Labels, func(i, j int) bool {
return seg.Labels[i].Species < seg.Labels[j].Species
})
s.Dirty = true
}
// ApplyCallTypeOnly sets the CallType on the current segment's first
// filter-matching label. Used after a Shift+primary keypress labeled the
// species and we now receive the secondary key for the calltype.
// No-op if there is no matching label to update.
func (s *ClassifyState) ApplyCallTypeOnly(callType string) {
seg := s.CurrentSegment()
if seg == nil {
return
}
df := s.CurrentFile()
if df == nil {
return
}
filterLabels := seg.GetFilterLabels(s.Config.Filter)
if len(filterLabels) == 0 {
return
}
df.Meta.Reviewer = s.Config.Reviewer
filterLabels[0].CallType = callType
s.Dirty = true
}
// HasSecondary reports whether the given primary key has any secondary
// (calltype) bindings configured.
func (s *ClassifyState) HasSecondary(primaryKey string) bool {
return len(s.Config.SecondaryBindings[primaryKey]) > 0
}