SPHUX2CTF2S2TXEO3TRHNY7NIJ42G5KEWZRHQPZWXP77SJS5WJUAC XSITZX775O4AB2YYSBF7XOOYFUCB6IS25HG4NIO2PQLKFHTTJLZAC IOGCFEJZXNXE2YP5F6Q2WXQ2EPBOA64JTQTSIZSCIF67WLF2QA6AC ULWBYMSXYZEE7BJQ2B4HTX2JAABBNJWZTUFIQ56NF5RWXODXRTHQC DBOROCRFD6A5SJBMFYFEJI5S5M77X4EFEK6KDQWA5QDMQJKIHRWQC EW7VBNMGWFBC73ZUDLB4LIK2HWFKA74ZUTUDG4J575ZQHEFHW4UQC SDBVLSDDRPQF62XXKJKM2RQLMXOKKHOYRVUF6DIUDFRYCGL2DW3QC MFURT7K56GUJH64Z6XRKWKUI4VZZQYR72U2QEQNWR4B3TSUEXPQAC IFVRAERTCCDICNTYTG3TX2WASB6RXQQEJWWXQMQZJSQDQ3HLE5OQC W3A2EECCD23SVHJZN6MXPH2PAVFHH5CNFD2XHPQRRW6M4GUTG3FAC case "ctrl+d":// Toggle bookmarkm.state.ToggleBookmark()if err := m.state.Save(); err != nil {m.err = err.Error()}return m, nilcase "ctrl+n":// Next bookmarkif m.state.Player != nil {m.state.Player.Stop()}if m.state.NextBookmark() {return m, m.segmentChangeCmd()}m.err = "No bookmarks found"return m, nilcase "ctrl+p":// Previous bookmarkif m.state.Player != nil {m.state.Player.Stop()}if m.state.PrevBookmark() {return m, m.segmentChangeCmd()}m.err = "No bookmarks found"return m, nil
b.WriteString(helpStyle.Render("[esc]quit [,]prev [.]next [space]comment [enter]play [shift+enter]½speed"))
b.WriteString(helpStyle.Render("[esc]quit [,]prev [.]next [space]comment [ctrl+d]bookmark [ctrl+n]next-bk [ctrl+p]prev-bk [enter]play [shift+enter]½speed"))
}// getFilterLabel returns the label matching the current filter, or first label if no filter.func (s *ClassifyState) getFilterLabel(seg *utils.Segment) *utils.Label {if s.Config.Filter == "" {if len(seg.Labels) > 0 {return seg.Labels[0]}return nil}for _, label := range seg.Labels {if label.Filter == s.Config.Filter {return label}}return nil}// getOrCreateFilterLabel gets existing label or creates new one for the current filter.func (s *ClassifyState) getOrCreateFilterLabel(seg *utils.Segment) *utils.Label {label := s.getFilterLabel(seg)if label != nil {return label}// Create new labellabel = &utils.Label{Species: "Don't Know",Certainty: 0,Filter: s.Config.Filter,}seg.Labels = append(seg.Labels, label)s.Dirty = truereturn label}// HasBookmark returns true if current segment has a bookmark on the filter label.func (s *ClassifyState) HasBookmark() bool {seg := s.CurrentSegment()if seg == nil {return false}label := s.getFilterLabel(seg)return label != nil && label.Bookmark}// ToggleBookmark toggles the bookmark on the current segment's filter label.func (s *ClassifyState) ToggleBookmark() {seg := s.CurrentSegment()if seg == nil {return}df := s.CurrentFile()if df == nil {return}// Set reviewerdf.Meta.Reviewer = s.Config.Reviewerlabel := s.getOrCreateFilterLabel(seg)label.Bookmark = !label.Bookmarks.Dirty = true}// NextBookmark navigates to the next bookmark, wrapping around if needed.// Returns false if no bookmarks found (back at start position).func (s *ClassifyState) NextBookmark() bool {startFile := s.FileIdxstartSeg := s.SegmentIdxfirst := truefor {// Advance to next segmentif !s.nextSegmentRaw() {// Wrap to start of folders.FileIdx = 0s.SegmentIdx = 0}// Check if we've looped back to startif !first && s.FileIdx == startFile && s.SegmentIdx == startSeg {return false // full circle, no bookmark found}first = false// Check if current segment has bookmarkif s.hasFilterBookmark() {return true}}
for {// Move to previous segmentif !s.prevSegmentRaw() {// Wrap to end of folders.FileIdx = len(s.DataFiles) - 1df := s.DataFiles[s.FileIdx]segs := s.getFilteredSegments(df)s.SegmentIdx = max(len(segs)-1, 0)}// Check if we've looped back to startif !first && s.FileIdx == startFile && s.SegmentIdx == startSeg {return false // full circle, no bookmark found}first = false// Check if current segment has bookmarkif s.hasFilterBookmark() {return true}}}// nextSegmentRaw moves to next segment without wrapping, returns false if at end.func (s *ClassifyState) nextSegmentRaw() bool {df := s.CurrentFile()if df == nil {return false}segs := s.getFilteredSegments(df)if s.SegmentIdx+1 < len(segs) {s.SegmentIdx++return true}// Move to next fileif s.FileIdx+1 < len(s.DataFiles) {s.FileIdx++s.SegmentIdx = 0return true}return false}// prevSegmentRaw moves to previous segment without wrapping, returns false if at start.func (s *ClassifyState) prevSegmentRaw() bool {if s.SegmentIdx > 0 {s.SegmentIdx--return true}// Move to previous fileif s.FileIdx > 0 {s.FileIdx--df := s.CurrentFile()segs := s.getFilteredSegments(df)s.SegmentIdx = max(len(segs)-1, 0)return true}return false}// hasFilterBookmark checks if current segment has bookmark on filter-matching label.func (s *ClassifyState) hasFilterBookmark() bool {seg := s.CurrentSegment()if seg == nil {return false}return s.segmentHasBookmark(seg)}// segmentHasBookmark checks if a segment has a bookmark on filter-matching label.func (s *ClassifyState) segmentHasBookmark(seg *utils.Segment) bool {label := s.getFilterLabel(seg)return label != nil && label.Bookmark}
## [2026-03-09] Bookmark Navigation in TUI**New feature:** Bookmark segments for later review.**Changes:**- `utils/data_file.go` — Added `Bookmark bool` to Label struct- `tools/calls_classify.go` — Added bookmark methods- `tui/classify.go` — Added key handlers and display**Format** (stored in label):```json[0, 3, 0, 16000, [{"species": "Kiwi", "certainty": 90, "filter": "BirdNET", "bookmark": true}]]```**Key bindings:**| Key | Action ||-----|--------|| `Ctrl+D` | Toggle bookmark on current segment || `Ctrl+N` | Navigate to next bookmark (wraps around) || `Ctrl+P` | Navigate to previous bookmark (wraps around) |