package detailsviewer import ( "log/slog" "tasksquire/common" "tasksquire/taskwarrior" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/glamour" "github.com/charmbracelet/lipgloss" ) // DetailsViewer is a reusable component for displaying task details type DetailsViewer struct { common *common.Common viewport viewport.Model task *taskwarrior.Task focused bool width int height int } // New creates a new DetailsViewer component func New(com *common.Common) *DetailsViewer { return &DetailsViewer{ common: com, viewport: viewport.New(0, 0), focused: false, } } // SetTask updates the task to display func (d *DetailsViewer) SetTask(task *taskwarrior.Task) { d.task = task d.updateContent() } // Focus sets the component to focused state (for future interactivity) func (d *DetailsViewer) Focus() { d.focused = true } // Blur sets the component to blurred state func (d *DetailsViewer) Blur() { d.focused = false } // IsFocused returns whether the component is focused func (d *DetailsViewer) IsFocused() bool { return d.focused } // SetSize implements common.Component func (d *DetailsViewer) SetSize(width, height int) { d.width = width d.height = height // Account for border and padding (4 chars horizontal, 4 lines vertical) d.viewport.Width = max(width-4, 0) d.viewport.Height = max(height-4, 0) // Refresh content with new width d.updateContent() } // Init implements tea.Model func (d *DetailsViewer) Init() tea.Cmd { return nil } // Update implements tea.Model func (d *DetailsViewer) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd d.viewport, cmd = d.viewport.Update(msg) return d, cmd } // View implements tea.Model func (d *DetailsViewer) View() string { // Title bar titleStyle := lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color("252")) helpStyle := lipgloss.NewStyle(). Foreground(lipgloss.Color("240")) header := lipgloss.JoinHorizontal( lipgloss.Left, titleStyle.Render("Details"), " ", helpStyle.Render("(↑/↓ scroll, v/ESC close)"), ) // Container style containerStyle := lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("240")). Padding(0, 1). Width(d.width). Height(d.height) // Optional: highlight border when focused (for future interactivity) if d.focused { containerStyle = containerStyle. BorderForeground(lipgloss.Color("86")) } content := lipgloss.JoinVertical( lipgloss.Left, header, d.viewport.View(), ) return containerStyle.Render(content) } // updateContent refreshes the viewport content based on current task func (d *DetailsViewer) updateContent() { if d.task == nil { d.viewport.SetContent("(No task selected)") return } detailsValue := "" if details, ok := d.task.Udas["details"]; ok && details != nil { detailsValue = details.(string) } if detailsValue == "" { d.viewport.SetContent("(No details for this task)") return } // Render markdown with glamour renderer, err := glamour.NewTermRenderer( glamour.WithAutoStyle(), glamour.WithWordWrap(d.viewport.Width), ) if err != nil { slog.Error("failed to create markdown renderer", "error", err) // Fallback to plain text wrapped := lipgloss.NewStyle(). Width(d.viewport.Width). Render(detailsValue) d.viewport.SetContent(wrapped) d.viewport.GotoTop() return } rendered, err := renderer.Render(detailsValue) if err != nil { slog.Error("failed to render markdown", "error", err) // Fallback to plain text wrapped := lipgloss.NewStyle(). Width(d.viewport.Width). Render(detailsValue) d.viewport.SetContent(wrapped) d.viewport.GotoTop() return } d.viewport.SetContent(rendered) d.viewport.GotoTop() } // Helper function func max(a, b int) int { if a > b { return a } return b }