Files
klp/internal/tui/tui.go
2026-01-27 22:01:30 -06:00

232 lines
4.6 KiB
Go

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
}