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))
}