175 lines
3.8 KiB
Go
175 lines
3.8 KiB
Go
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
|
|
}
|