Fork channel

Create a new channel as a copy of main.

Rename channel

Rename main to:

Delete channel

Delete main? This cannot be undone.

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
}