init commit
This commit is contained in:
53
internal/tui/styles.go
Normal file
53
internal/tui/styles.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package tui
|
||||
|
||||
import "github.com/charmbracelet/lipgloss"
|
||||
|
||||
var (
|
||||
// Colors
|
||||
secondaryColor = lipgloss.Color("241") // Gray
|
||||
dimColor = lipgloss.Color("245") // Dimmer gray
|
||||
|
||||
// Title
|
||||
TitleStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("213")) // Magenta/pink
|
||||
|
||||
// ID styles
|
||||
IDStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("82")) // Bright green
|
||||
|
||||
IDStyleSelected = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("154")) // Even brighter green
|
||||
|
||||
// DateTime styles
|
||||
DateTimeStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("244")) // Medium gray
|
||||
|
||||
DateTimeStyleSelected = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("250")) // Lighter
|
||||
|
||||
// Content styles
|
||||
ContentStyle = lipgloss.NewStyle().
|
||||
Foreground(dimColor)
|
||||
|
||||
ContentStyleSelected = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("252")) // Brighter when selected
|
||||
|
||||
// Cursor
|
||||
CursorStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("212"))
|
||||
|
||||
// Help
|
||||
HelpStyle = lipgloss.NewStyle().
|
||||
Foreground(secondaryColor).
|
||||
MarginTop(1)
|
||||
|
||||
// No results
|
||||
NoResultsStyle = lipgloss.NewStyle().
|
||||
Foreground(secondaryColor).
|
||||
Italic(true).
|
||||
MarginTop(1)
|
||||
)
|
||||
231
internal/tui/tui.go
Normal file
231
internal/tui/tui.go
Normal file
@@ -0,0 +1,231 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user