package tui import ( "fmt" "strings" tea "github.com/charmbracelet/bubbletea" "github.com/yeho/klp/internal/database" ) type Model struct { entries []*database.Entry db *database.Database cursor int selected *database.Entry title string quitting bool width int height int } func New(entries []*database.Entry, db *database.Database, title string) Model { return Model{ entries: entries, db: db, cursor: 0, title: title, } } func (m Model) Init() tea.Cmd { return nil } func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c", "q", "esc": m.quitting = true return m, tea.Quit case "enter": if len(m.entries) > 0 && m.cursor < len(m.entries) { m.selected = m.entries[m.cursor] } return m, tea.Quit case "up", "k": if m.cursor > 0 { m.cursor-- } case "down", "j": if m.cursor < len(m.entries)-1 { m.cursor++ } case "home", "g": m.cursor = 0 case "end", "G": m.cursor = len(m.entries) - 1 case "d": if len(m.entries) > 0 && m.cursor < len(m.entries) && m.db != nil { entry := m.entries[m.cursor] if err := m.db.Delete(entry.ID); err == nil { // Remove from local list m.entries = append(m.entries[:m.cursor], m.entries[m.cursor+1:]...) // Adjust cursor if needed if m.cursor >= len(m.entries) && m.cursor > 0 { m.cursor-- } // Update title with new count m.title = fmt.Sprintf("Clipboard History (%d entries)", len(m.entries)) } } } case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height } return m, nil } func (m Model) View() string { if len(m.entries) == 0 { return NoResultsStyle.Render("No clipboard entries found") } var b strings.Builder // Title b.WriteString(TitleStyle.Render(m.title)) b.WriteString("\n") // Calculate visible window maxVisible := 10 if m.height > 0 { maxVisible = m.height - 6 // Account for title, help, margins if maxVisible < 5 { maxVisible = 5 } } start := 0 end := len(m.entries) if len(m.entries) > maxVisible { start = m.cursor - maxVisible/2 if start < 0 { start = 0 } end = start + maxVisible if end > len(m.entries) { end = len(m.entries) start = end - maxVisible if start < 0 { start = 0 } } } // Calculate max content width maxContentWidth := 60 if m.width > 0 { // Reserve space for cursor, ID, datetime, and margins maxContentWidth = m.width - 30 if maxContentWidth < 20 { maxContentWidth = 20 } } for i := start; i < end; i++ { entry := m.entries[i] isSelected := i == m.cursor // Cursor indicator cursor := " " if isSelected { cursor = CursorStyle.Render("> ") } // ID var id string if isSelected { id = IDStyleSelected.Render(entry.ID) } else { id = IDStyle.Render(entry.ID) } // DateTime dateStr := entry.DateTime.Format("01/02 15:04") var dateTime string if isSelected { dateTime = DateTimeStyleSelected.Render(dateStr) } else { dateTime = DateTimeStyle.Render(dateStr) } // Content preview (truncate and clean) preview := cleanPreview(entry.Value, maxContentWidth) var content string if isSelected { content = ContentStyleSelected.Render(preview) } else { content = ContentStyle.Render(preview) } line := fmt.Sprintf("%s%s %s %s", cursor, id, dateTime, content) b.WriteString(line) b.WriteString("\n") } // Scroll indicator if len(m.entries) > maxVisible { scrollInfo := fmt.Sprintf("(%d-%d of %d)", start+1, end, len(m.entries)) b.WriteString(DateTimeStyle.Render(scrollInfo)) b.WriteString("\n") } // Help help := "\n j/k up/down enter select d delete q quit" b.WriteString(HelpStyle.Render(help)) return b.String() } func cleanPreview(s string, maxLen int) string { // Replace newlines with spaces result := strings.ReplaceAll(s, "\n", " ") result = strings.ReplaceAll(result, "\r", "") result = strings.ReplaceAll(result, "\t", " ") // Collapse multiple spaces for strings.Contains(result, " ") { result = strings.ReplaceAll(result, " ", " ") } result = strings.TrimSpace(result) // Truncate if len(result) > maxLen { result = result[:maxLen-3] + "..." } return result } func (m Model) Selected() *database.Entry { return m.selected } func (m Model) Quitting() bool { return m.quitting } func Run(entries []*database.Entry, db *database.Database, title string) (*database.Entry, error) { model := New(entries, db, title) p := tea.NewProgram(model) finalModel, err := p.Run() if err != nil { return nil, err } m := finalModel.(Model) return m.Selected(), nil }