Add interactive Markdown checklist toggles in the ticket preview.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
|||||||
|
package ticket
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// taskCheckboxRe matches GFM/YouTrack-style task list items:
|
||||||
|
//
|
||||||
|
// - [ ] unchecked
|
||||||
|
// - [x] checked
|
||||||
|
// * [X] also accepted
|
||||||
|
var taskCheckboxRe = regexp.MustCompile(`^(\s*)([-*+])\s+\[([ xX])\](\s*)(.*)$`)
|
||||||
|
|
||||||
|
// Checkbox is one task-list item inside a Markdown description.
|
||||||
|
type Checkbox struct {
|
||||||
|
Line int // 0-based line index in the source
|
||||||
|
Checked bool
|
||||||
|
Text string // label after the checkbox marker
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListCheckboxes returns all task-list items in markdown source order.
|
||||||
|
func ListCheckboxes(markdown string) []Checkbox {
|
||||||
|
if markdown == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
lines := strings.Split(markdown, "\n")
|
||||||
|
var out []Checkbox
|
||||||
|
for i, line := range lines {
|
||||||
|
m := taskCheckboxRe.FindStringSubmatch(line)
|
||||||
|
if m == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, Checkbox{
|
||||||
|
Line: i,
|
||||||
|
Checked: m[3] == "x" || m[3] == "X",
|
||||||
|
Text: m[5],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToggleCheckbox flips the checked state of the checkbox at index
|
||||||
|
// (0-based among ListCheckboxes results) and returns the updated markdown.
|
||||||
|
func ToggleCheckbox(markdown string, index int) (string, error) {
|
||||||
|
boxes := ListCheckboxes(markdown)
|
||||||
|
if index < 0 || index >= len(boxes) {
|
||||||
|
return markdown, fmt.Errorf("checkbox %d: not found", index)
|
||||||
|
}
|
||||||
|
lines := strings.Split(markdown, "\n")
|
||||||
|
lineIdx := boxes[index].Line
|
||||||
|
m := taskCheckboxRe.FindStringSubmatch(lines[lineIdx])
|
||||||
|
if m == nil {
|
||||||
|
return markdown, fmt.Errorf("checkbox %d: line changed", index)
|
||||||
|
}
|
||||||
|
mark := " "
|
||||||
|
if !(m[3] == "x" || m[3] == "X") {
|
||||||
|
mark = "x"
|
||||||
|
}
|
||||||
|
lines[lineIdx] = m[1] + m[2] + " [" + mark + "]" + m[4] + m[5]
|
||||||
|
return strings.Join(lines, "\n"), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package ticket
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestListCheckboxes(t *testing.T) {
|
||||||
|
src := `# Steps
|
||||||
|
|
||||||
|
- [ ] one
|
||||||
|
- [x] two
|
||||||
|
* [X] three
|
||||||
|
- [ ] nested
|
||||||
|
|
||||||
|
Not a box: [ ] plain
|
||||||
|
`
|
||||||
|
got := ListCheckboxes(src)
|
||||||
|
if len(got) != 4 {
|
||||||
|
t.Fatalf("len=%d want 4: %+v", len(got), got)
|
||||||
|
}
|
||||||
|
if got[0].Text != "one" || got[0].Checked || got[0].Line != 2 {
|
||||||
|
t.Fatalf("first: %+v", got[0])
|
||||||
|
}
|
||||||
|
if !got[1].Checked || got[1].Text != "two" {
|
||||||
|
t.Fatalf("second: %+v", got[1])
|
||||||
|
}
|
||||||
|
if !got[2].Checked || got[2].Text != "three" {
|
||||||
|
t.Fatalf("third: %+v", got[2])
|
||||||
|
}
|
||||||
|
if got[3].Text != "nested" || got[3].Checked || got[3].Line != 5 {
|
||||||
|
t.Fatalf("nested: %+v", got[3])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToggleCheckbox(t *testing.T) {
|
||||||
|
src := "- [ ] a\n- [x] b\n"
|
||||||
|
out, err := ToggleCheckbox(src, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if out != "- [x] a\n- [x] b\n" {
|
||||||
|
t.Fatalf("toggle 0: %q", out)
|
||||||
|
}
|
||||||
|
out, err = ToggleCheckbox(out, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if out != "- [x] a\n- [ ] b\n" {
|
||||||
|
t.Fatalf("toggle 1: %q", out)
|
||||||
|
}
|
||||||
|
if _, err := ToggleCheckbox(src, 9); err == nil {
|
||||||
|
t.Fatal("expected error for bad index")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToggleCheckboxPreservesIndentAndBullet(t *testing.T) {
|
||||||
|
src := " * [X] deep"
|
||||||
|
out, err := ToggleCheckbox(src, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if out != " * [ ] deep" {
|
||||||
|
t.Fatalf("got %q", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -382,6 +382,23 @@ func (m Model) updateList(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
m.mode = ModeTimeEntries
|
m.mode = ModeTimeEntries
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
|
case key.Matches(msg, m.keys.ChecklistNext):
|
||||||
|
m.clearStatus()
|
||||||
|
m.tickets.MoveChecklistDown()
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case key.Matches(msg, m.keys.ChecklistPrev):
|
||||||
|
m.clearStatus()
|
||||||
|
m.tickets.MoveChecklistUp()
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case key.Matches(msg, m.keys.Toggle):
|
||||||
|
m.clearStatus()
|
||||||
|
if err := m.toggleSelectedCheckbox(); err != nil {
|
||||||
|
m.setError(err)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
|
||||||
case key.Matches(msg, m.keys.Down):
|
case key.Matches(msg, m.keys.Down):
|
||||||
m.tickets.MoveDown()
|
m.tickets.MoveDown()
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -985,6 +1002,52 @@ func (m *Model) applyStatus(st ticket.Status) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Model) toggleSelectedCheckbox() error {
|
||||||
|
t, ok := m.tickets.Selected()
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("no ticket selected")
|
||||||
|
}
|
||||||
|
boxes := ticket.ListCheckboxes(t.Description)
|
||||||
|
if len(boxes) == 0 {
|
||||||
|
return fmt.Errorf("no checkboxes in description")
|
||||||
|
}
|
||||||
|
checkIdx := m.tickets.ChecklistCursor
|
||||||
|
if checkIdx < 0 || checkIdx >= len(boxes) {
|
||||||
|
checkIdx = 0
|
||||||
|
}
|
||||||
|
newDesc, err := ticket.ToggleCheckbox(t.Description, checkIdx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
after := t
|
||||||
|
after.Description = newDesc
|
||||||
|
if err := m.exec.Do(&command.UpdateTicket{
|
||||||
|
Store: m.store,
|
||||||
|
Before: t,
|
||||||
|
After: after,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ticketIdx := m.tickets.CursorIndex()
|
||||||
|
if err := m.reloadTickets(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ticketIdx < m.tickets.Len() {
|
||||||
|
m.tickets.Select(ticketIdx)
|
||||||
|
}
|
||||||
|
m.tickets.SetChecklistCursor(checkIdx)
|
||||||
|
state := "unchecked"
|
||||||
|
if !boxes[checkIdx].Checked {
|
||||||
|
state = "checked"
|
||||||
|
}
|
||||||
|
label := strings.TrimSpace(boxes[checkIdx].Text)
|
||||||
|
if label == "" {
|
||||||
|
label = fmt.Sprintf("#%d", checkIdx+1)
|
||||||
|
}
|
||||||
|
m.setStatus(fmt.Sprintf("%s %s", state, label))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Model) undo() error {
|
func (m *Model) undo() error {
|
||||||
name := m.exec.LastUndoName()
|
name := m.exec.LastUndoName()
|
||||||
if err := m.exec.Undo(); err != nil {
|
if err := m.exec.Undo(); err != nil {
|
||||||
|
|||||||
@@ -98,6 +98,9 @@ func (m Model) helpEntries() []helpEntry {
|
|||||||
default:
|
default:
|
||||||
return []helpEntry{
|
return []helpEntry{
|
||||||
{"j/k", "navigate"},
|
{"j/k", "navigate"},
|
||||||
|
{"J/K", "checklist"},
|
||||||
|
{"S-↑/↓", "checklist"},
|
||||||
|
{"space", "check"},
|
||||||
{"n", "new"},
|
{"n", "new"},
|
||||||
{"e", "edit"},
|
{"e", "edit"},
|
||||||
{"t/T", "timer/log"},
|
{"t/T", "timer/log"},
|
||||||
|
|||||||
+20
-10
@@ -17,16 +17,18 @@ type KeyMap struct {
|
|||||||
Delete key.Binding // x — confirm first
|
Delete key.Binding // x — confirm first
|
||||||
DeleteForce key.Binding // X — immediate
|
DeleteForce key.Binding // X — immediate
|
||||||
ConfirmYes key.Binding
|
ConfirmYes key.Binding
|
||||||
Toggle key.Binding // space — toggle multi-select
|
Toggle key.Binding // space — toggle multi-select / checklist item
|
||||||
Timer key.Binding // t — start/stop timer
|
ChecklistPrev key.Binding // K / shift+up — previous checkbox
|
||||||
TimeManual key.Binding // T — manual duration
|
ChecklistNext key.Binding // J / shift+down — next checkbox
|
||||||
TimeEntries key.Binding // m — manage time entries
|
Timer key.Binding // t — start/stop timer
|
||||||
Report key.Binding // r — time report
|
TimeManual key.Binding // T — manual duration
|
||||||
Undo key.Binding
|
TimeEntries key.Binding // m — manage time entries
|
||||||
Redo key.Binding
|
Report key.Binding // r — time report
|
||||||
Quit key.Binding
|
Undo key.Binding
|
||||||
Enter key.Binding
|
Redo key.Binding
|
||||||
Escape key.Binding
|
Quit key.Binding
|
||||||
|
Enter key.Binding
|
||||||
|
Escape key.Binding
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultKeyMap returns the default key bindings.
|
// DefaultKeyMap returns the default key bindings.
|
||||||
@@ -88,6 +90,14 @@ func DefaultKeyMap() KeyMap {
|
|||||||
key.WithKeys(" "),
|
key.WithKeys(" "),
|
||||||
key.WithHelp("space", "toggle"),
|
key.WithHelp("space", "toggle"),
|
||||||
),
|
),
|
||||||
|
ChecklistPrev: key.NewBinding(
|
||||||
|
key.WithKeys("K", "shift+up"),
|
||||||
|
key.WithHelp("J/K", "checklist"),
|
||||||
|
),
|
||||||
|
ChecklistNext: key.NewBinding(
|
||||||
|
key.WithKeys("J", "shift+down"),
|
||||||
|
key.WithHelp("J/K", "checklist"),
|
||||||
|
),
|
||||||
Timer: key.NewBinding(
|
Timer: key.NewBinding(
|
||||||
key.WithKeys("t"),
|
key.WithKeys("t"),
|
||||||
key.WithHelp("t", "timer"),
|
key.WithHelp("t", "timer"),
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package views
|
package views
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/charmbracelet/glamour"
|
"github.com/charmbracelet/glamour"
|
||||||
"github.com/charmbracelet/glamour/styles"
|
"github.com/charmbracelet/glamour/styles"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/muesli/termenv"
|
"github.com/muesli/termenv"
|
||||||
|
|
||||||
|
"task/internal/ticket"
|
||||||
)
|
)
|
||||||
|
|
||||||
type mdCacheKey struct {
|
type mdCacheKey struct {
|
||||||
@@ -79,6 +83,78 @@ func getRenderer(width int) (*glamour.TermRenderer, error) {
|
|||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RenderDescription renders ticket Markdown with interactive task-list items.
|
||||||
|
// checklistCursor is the 0-based index among checkboxes (highlight); ignored when
|
||||||
|
// there are none.
|
||||||
|
func RenderDescription(src string, width int, checklistCursor int) string {
|
||||||
|
if strings.TrimSpace(src) == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
boxes := ticket.ListCheckboxes(src)
|
||||||
|
if len(boxes) == 0 {
|
||||||
|
return RenderMarkdown(src, width)
|
||||||
|
}
|
||||||
|
|
||||||
|
boxByLine := make(map[int]int, len(boxes))
|
||||||
|
for i, b := range boxes {
|
||||||
|
boxByLine[b.Line] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(src, "\n")
|
||||||
|
var parts []string
|
||||||
|
var mdBuf []string
|
||||||
|
flushMD := func() {
|
||||||
|
if len(mdBuf) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
block := strings.Join(mdBuf, "\n")
|
||||||
|
if strings.TrimSpace(block) != "" {
|
||||||
|
parts = append(parts, RenderMarkdown(block, width))
|
||||||
|
}
|
||||||
|
mdBuf = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, line := range lines {
|
||||||
|
if bi, ok := boxByLine[i]; ok {
|
||||||
|
flushMD()
|
||||||
|
parts = append(parts, renderCheckboxLine(boxes[bi], bi == checklistCursor, width))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mdBuf = append(mdBuf, line)
|
||||||
|
}
|
||||||
|
flushMD()
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left, parts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderCheckboxLine(box ticket.Checkbox, selected bool, width int) string {
|
||||||
|
// ASCII markers — widely supported; bold + padding reads a notch bigger.
|
||||||
|
mark := "[ ]"
|
||||||
|
if box.Checked {
|
||||||
|
mark = "[x]"
|
||||||
|
}
|
||||||
|
prefix := " "
|
||||||
|
if selected {
|
||||||
|
prefix = "> "
|
||||||
|
}
|
||||||
|
label := strings.TrimSpace(box.Text)
|
||||||
|
avail := max(10, width) - lipgloss.Width(prefix+mark+" ")
|
||||||
|
label = truncateOneLine(label, avail)
|
||||||
|
|
||||||
|
markStyle := lipgloss.NewStyle().Bold(true)
|
||||||
|
labelStyle := lipgloss.NewStyle()
|
||||||
|
if selected {
|
||||||
|
markStyle = markStyle.Foreground(lipgloss.Color("39"))
|
||||||
|
labelStyle = labelStyle.Bold(true).Foreground(lipgloss.Color("39"))
|
||||||
|
} else if box.Checked {
|
||||||
|
labelStyle = labelStyle.Faint(true)
|
||||||
|
}
|
||||||
|
line := prefix + markStyle.Render(mark) + " " + labelStyle.Render(label)
|
||||||
|
return lipgloss.NewStyle().Width(max(10, width)).Render(line)
|
||||||
|
}
|
||||||
|
|
||||||
// ClearMarkdownCache drops cached renders (e.g. after theme change).
|
// ClearMarkdownCache drops cached renders (e.g. after theme change).
|
||||||
func ClearMarkdownCache() {
|
func ClearMarkdownCache() {
|
||||||
mdMu.Lock()
|
mdMu.Lock()
|
||||||
|
|||||||
@@ -30,3 +30,14 @@ func TestRenderMarkdownFastWithoutAutoStyle(t *testing.T) {
|
|||||||
}
|
}
|
||||||
t.Logf("first=%v cached=%v", elapsed, time.Since(start))
|
t.Logf("first=%v cached=%v", elapsed, time.Since(start))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRenderDescriptionHighlightsCheckbox(t *testing.T) {
|
||||||
|
src := "Intro\n\n- [ ] one\n- [x] two\n"
|
||||||
|
out := views.RenderDescription(src, 40, 0)
|
||||||
|
if !strings.Contains(out, "one") || !strings.Contains(out, "[ ]") {
|
||||||
|
t.Fatalf("expected unchecked item in output: %q", out)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, "two") || !strings.Contains(out, "[x]") {
|
||||||
|
t.Fatalf("expected checked item in output: %q", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,13 +26,14 @@ type TicketListStyles struct {
|
|||||||
|
|
||||||
// TicketList is a custom viewport list of ticket cards.
|
// TicketList is a custom viewport list of ticket cards.
|
||||||
type TicketList struct {
|
type TicketList struct {
|
||||||
Items []ticket.Ticket
|
Items []ticket.Ticket
|
||||||
Cursor int
|
Cursor int
|
||||||
Offset int
|
Offset int
|
||||||
Width int
|
Width int
|
||||||
Height int
|
Height int
|
||||||
Styles TicketListStyles
|
Styles TicketListStyles
|
||||||
SpentByID map[string]time.Duration // optional; shown on selected card
|
SpentByID map[string]time.Duration // optional; shown on selected card
|
||||||
|
ChecklistCursor int // index into checkboxes of the selected ticket
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSize updates the viewport dimensions.
|
// SetSize updates the viewport dimensions.
|
||||||
@@ -49,6 +50,7 @@ func (l *TicketList) SetItems(items []ticket.Ticket) {
|
|||||||
l.Cursor = max(0, len(l.Items)-1)
|
l.Cursor = max(0, len(l.Items)-1)
|
||||||
}
|
}
|
||||||
l.Offset = 0
|
l.Offset = 0
|
||||||
|
l.clampChecklistCursor()
|
||||||
l.ensureVisible()
|
l.ensureVisible()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +72,7 @@ func (l *TicketList) Len() int { return len(l.Items) }
|
|||||||
func (l *TicketList) MoveDown() {
|
func (l *TicketList) MoveDown() {
|
||||||
if l.Cursor < len(l.Items)-1 {
|
if l.Cursor < len(l.Items)-1 {
|
||||||
l.Cursor++
|
l.Cursor++
|
||||||
|
l.ChecklistCursor = 0
|
||||||
l.ensureVisible()
|
l.ensureVisible()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,6 +81,7 @@ func (l *TicketList) MoveDown() {
|
|||||||
func (l *TicketList) MoveUp() {
|
func (l *TicketList) MoveUp() {
|
||||||
if l.Cursor > 0 {
|
if l.Cursor > 0 {
|
||||||
l.Cursor--
|
l.Cursor--
|
||||||
|
l.ChecklistCursor = 0
|
||||||
l.ensureVisible()
|
l.ensureVisible()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,10 +91,61 @@ func (l *TicketList) Select(idx int) {
|
|||||||
if idx < 0 || idx >= len(l.Items) {
|
if idx < 0 || idx >= len(l.Items) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if l.Cursor != idx {
|
||||||
|
l.ChecklistCursor = 0
|
||||||
|
}
|
||||||
l.Cursor = idx
|
l.Cursor = idx
|
||||||
|
l.clampChecklistCursor()
|
||||||
l.ensureVisible()
|
l.ensureVisible()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChecklistLen returns how many task-list items the selected ticket has.
|
||||||
|
func (l *TicketList) ChecklistLen() int {
|
||||||
|
t, ok := l.Selected()
|
||||||
|
if !ok {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return len(ticket.ListCheckboxes(t.Description))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveChecklistDown advances the checkbox cursor (wraps).
|
||||||
|
func (l *TicketList) MoveChecklistDown() {
|
||||||
|
n := l.ChecklistLen()
|
||||||
|
if n == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.ChecklistCursor = (l.ChecklistCursor + 1) % n
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveChecklistUp moves the checkbox cursor up (wraps).
|
||||||
|
func (l *TicketList) MoveChecklistUp() {
|
||||||
|
n := l.ChecklistLen()
|
||||||
|
if n == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.ChecklistCursor = (l.ChecklistCursor - 1 + n) % n
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetChecklistCursor sets the checkbox highlight index (clamped).
|
||||||
|
func (l *TicketList) SetChecklistCursor(idx int) {
|
||||||
|
l.ChecklistCursor = idx
|
||||||
|
l.clampChecklistCursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *TicketList) clampChecklistCursor() {
|
||||||
|
n := l.ChecklistLen()
|
||||||
|
if n == 0 {
|
||||||
|
l.ChecklistCursor = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if l.ChecklistCursor < 0 {
|
||||||
|
l.ChecklistCursor = 0
|
||||||
|
}
|
||||||
|
if l.ChecklistCursor >= n {
|
||||||
|
l.ChecklistCursor = n - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// View renders only the visible ticket cards.
|
// View renders only the visible ticket cards.
|
||||||
func (l *TicketList) View() string {
|
func (l *TicketList) View() string {
|
||||||
if l.Height < 1 {
|
if l.Height < 1 {
|
||||||
@@ -126,16 +181,15 @@ func (l *TicketList) renderCard(t ticket.Ticket, selected bool) string {
|
|||||||
|
|
||||||
var content string
|
var content string
|
||||||
if selected {
|
if selected {
|
||||||
// Expanded: headline + optional spent + glamour description.
|
// Expanded: headline + optional spent + description (interactive checkboxes).
|
||||||
content = headerLine
|
content = headerLine
|
||||||
if spent, ok := l.SpentByID[t.ID]; ok && spent > 0 {
|
if spent, ok := l.SpentByID[t.ID]; ok && spent > 0 {
|
||||||
content += "\n" + lipgloss.NewStyle().Faint(true).Render(
|
content += "\n" + lipgloss.NewStyle().Faint(true).Render(
|
||||||
"spent: "+ticket.FormatDurationHuman(spent),
|
"spent: "+ticket.FormatDurationHuman(spent),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
desc := strings.TrimSpace(t.Description)
|
if strings.TrimSpace(t.Description) != "" {
|
||||||
if desc != "" {
|
content += "\n" + RenderDescription(t.Description, innerWidth, l.ChecklistCursor)
|
||||||
content += "\n" + RenderMarkdown(desc, innerWidth)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
content = headerLine
|
content = headerLine
|
||||||
|
|||||||
Reference in New Issue
Block a user