Fork channel

Create a new channel as a copy of main.

Rename channel

Rename main to:

Delete channel

Delete main? This cannot be undone.

view.go
package tui

import (
	"fmt"
	"strings"

	tea "charm.land/bubbletea/v2"

	"skraak/datafile"
	"skraak/spectrogram"
	"skraak/tools/calls"
)

// View renders the TUI
func (m Model) View() tea.View {
	if m.quitting {
		var b strings.Builder
		_ = spectrogram.ClearImages(&b, m.protocol())
		b.WriteString("\nDone!\n")
		return tea.NewView(b.String())
	}

	var b strings.Builder

	df := m.state.CurrentFile()
	seg := m.state.CurrentSegment()
	total := m.state.TotalSegments()
	current := m.state.CurrentSegmentNumber()

	if df == nil || seg == nil {
		return tea.NewView("\nNo segments to review.\n")
	}

	m.renderHeader(&b)
	m.renderProgressBar(&b, current, total)
	m.renderSegmentInfo(&b, df, seg)
	m.renderLabels(&b, seg)

	// Label dialog (when active)
	if m.labelMode {
		m.renderLabelDialog(&b)
		return tea.NewView(b.String())
	}

	// Clip dialog (when active)
	if m.clipMode {
		m.renderClipDialog(&b)
		return tea.NewView(b.String())
	}

	// Comment dialog (when active)
	if m.commentMode {
		m.renderCommentDialog(&b)
		return tea.NewView(b.String())
	}

	// Error
	if m.err != "" {
		b.WriteString(errorStyle.Render(m.err))
	}

	v := tea.NewView(b.String())
	v.AltScreen = true
	return v
}

// renderHeader renders the keybindings help text
func (m Model) renderHeader(b *strings.Builder) {
	const wrapWidth = 80
	b.WriteString(helpStyle.Render(wrapText(m.bindingsHelp, wrapWidth)))
	b.WriteString("\n")
	b.WriteString(helpDarkStyle.Render(wrapText(keyHelpText, wrapWidth)))
	b.WriteString("\n\n")
}

// renderProgressBar renders the progress bar and file title
func (m Model) renderProgressBar(b *strings.Builder, current, total int) {
	progress := float64(current) / float64(total)
	barWidth := 30
	filled := int(progress * float64(barWidth))
	bar := strings.Repeat("", filled) + strings.Repeat("", barWidth-filled)

	df := m.state.CurrentFile()
	wavFile := strings.TrimSuffix(df.FilePath, ".data")
	wavFile = wavFile[strings.LastIndex(wavFile, "/")+1:]
	b.WriteString(titleStyle.Render(fmt.Sprintf(" %s [%s] %d/%d Segments ", wavFile, bar, current, total)))
	b.WriteString("\n\n")
}

// renderSegmentInfo renders segment timing and playback status
func (m Model) renderSegmentInfo(b *strings.Builder, df *datafile.DataFile, seg *datafile.Segment) {
	segInfo := fmt.Sprintf("Segment: %.1fs - %.1fs (%.1fs)", seg.StartTime, seg.EndTime, seg.EndTime-seg.StartTime)
	if m.state.HasBookmark() {
		segInfo += " [BOOKMARKED]"
	}
	if m.awaitingSecondaryFor != "" {
		segInfo += "  Waiting..."
	}
	if m.state.Player != nil && m.state.Player.IsPlaying() {
		if m.state.PlaybackSpeed == 0.5 {
			segInfo += "  ▶ Playing 0.5x..."
		} else {
			segInfo += "  ▶ Playing..."
		}
	}
	b.WriteString(segInfo)
	b.WriteString("\n\n")
}

// renderLabels renders the filter labels for the current segment
func (m Model) renderLabels(b *strings.Builder, seg *datafile.Segment) {
	filterLabels := seg.GetFilterLabels(m.state.Config.Filter)
	if len(filterLabels) > 0 {
		b.WriteString(labelStyle.Render("Labels:"))
		b.WriteString("\n")
		for _, l := range filterLabels {
			fmt.Fprintf(b, "%s\n", calls.FormatLabels([]*datafile.Label{l}, m.state.Config.Filter))
		}
	}
	b.WriteString("\n")
}

// renderLabelDialog renders the label input dialog
func (m Model) renderLabelDialog(b *strings.Builder) {
	before := m.labelText[:m.labelCursor]
	after := m.labelText[m.labelCursor:]
	inputLine := before + "" + after

	charCount := fmt.Sprintf("%d/%d", len(m.labelText), maxLabelLength)

	content := fmt.Sprintf("Label (Species+CallType):\n%s\n%s\n%s", inputLine, charCount, labelHelpText)
	b.WriteString(commentBoxStyle.Render(content))
}

// renderCommentDialog renders the comment input dialog
func (m Model) renderCommentDialog(b *strings.Builder) {
	// Build input line with cursor at correct position
	before := m.commentText[:m.commentCursor]
	after := m.commentText[m.commentCursor:]
	inputLine := before + "" + after

	charCount := fmt.Sprintf("%d/%d", len(m.commentText), maxCommentLength)

	// Render box
	content := fmt.Sprintf("Comment:\n%s\n%s\n%s", inputLine, charCount, commentHelpText)
	b.WriteString(commentBoxStyle.Render(content))
}

// renderClipDialog renders the clip prefix input dialog
func (m Model) renderClipDialog(b *strings.Builder) {
	inputLine := m.clipInput + ""

	// Render box
	content := fmt.Sprintf("Clip prefix:\n%s\n%s", inputLine, clipHelpText)
	b.WriteString(commentBoxStyle.Render(content))
}