1package styles
2
3import (
4 "time"
5
6 "github.com/charmbracelet/bubbles/key"
7 tea "github.com/charmbracelet/bubbletea"
8 "github.com/charmbracelet/lipgloss"
9)
10
11// CleanupRange represents a time range for history cleanup.
12type CleanupRange int
13
14const (
15 CleanupRangeLastHour CleanupRange = iota
16 CleanupRangeLast24Hours
17 CleanupRangeLast7Days
18 CleanupRangeLast30Days
19 CleanupRangeAll
20)
21
22// Cursor symbols for list items (using Unicode).
23const (
24 cursorEmpty = " "
25 cursorSelected = "\u25b8 " // ▸ Black right-pointing small triangle
26)
27
28// CleanupRangeInfo holds display info for a cleanup range.
29type CleanupRangeInfo struct {
30 Range CleanupRange
31 Label string
32 Description string
33}
34
35// AllCleanupRanges returns all available cleanup ranges.
36func AllCleanupRanges() []CleanupRangeInfo {
37 return []CleanupRangeInfo{
38 {CleanupRangeLastHour, "Last hour", "Delete entries from the last hour"},
39 {CleanupRangeLast24Hours, "Last 24 hours", "Delete entries from the last day"},
40 {CleanupRangeLast7Days, "Last 7 days", "Delete entries from the last week"},
41 {CleanupRangeLast30Days, "Last 30 days", "Delete entries from the last month"},
42 {CleanupRangeAll, "All time", "Delete all history entries"},
43 }
44}
45
46// CutoffTime returns the cutoff time for this range.
47func (r CleanupRange) CutoffTime() time.Time {
48 now := time.Now()
49 switch r {
50 case CleanupRangeLastHour:
51 return now.Add(-1 * time.Hour)
52 case CleanupRangeLast24Hours:
53 return now.Add(-24 * time.Hour)
54 case CleanupRangeLast7Days:
55 return now.Add(-7 * 24 * time.Hour)
56 case CleanupRangeLast30Days:
57 return now.Add(-30 * 24 * time.Hour)
58 case CleanupRangeAll:
59 return time.Time{} // Zero time = delete all
60 default:
61 return time.Time{}
62 }
63}
64
65// CleanupModel is a cleanup range selection modal.
66type CleanupModel struct {
67 Ranges []CleanupRangeInfo
68 Selected int
69 Confirmed bool
70 Canceled bool
71 theme *Theme
72}
73
74// CleanupKeyMap defines keybindings for cleanup modal.
75type CleanupKeyMap struct {
76 Up key.Binding
77 Down key.Binding
78 Confirm key.Binding
79 Cancel key.Binding
80}
81
82// DefaultCleanupKeyMap returns default keybindings.
83func DefaultCleanupKeyMap() CleanupKeyMap {
84 return CleanupKeyMap{
85 Up: key.NewBinding(key.WithKeys("up", "k"), key.WithHelp("↑/k", "up")),
86 Down: key.NewBinding(key.WithKeys("down", "j"), key.WithHelp("↓/j", "down")),
87 Confirm: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "confirm")),
88 Cancel: key.NewBinding(key.WithKeys("esc", "q"), key.WithHelp("esc", "cancel")),
89 }
90}
91
92// NewCleanup creates a new cleanup modal.
93func NewCleanup(theme *Theme) CleanupModel {
94 return CleanupModel{
95 Ranges: AllCleanupRanges(),
96 Selected: 0,
97 theme: theme,
98 }
99}
100
101// Init implements tea.Model.
102func (m CleanupModel) Init() tea.Cmd {
103 if m.theme == nil {
104 return nil
105 }
106 return nil
107}
108
109// Update implements tea.Model.
110func (m CleanupModel) Update(msg tea.Msg) (CleanupModel, tea.Cmd) {
111 keys := DefaultCleanupKeyMap()
112
113 switch msg := msg.(type) {
114 case tea.KeyMsg:
115 switch {
116 case key.Matches(msg, keys.Up):
117 if m.Selected > 0 {
118 m.Selected--
119 }
120 case key.Matches(msg, keys.Down):
121 if m.Selected < len(m.Ranges)-1 {
122 m.Selected++
123 }
124 case key.Matches(msg, keys.Confirm):
125 m.Confirmed = true
126 case key.Matches(msg, keys.Cancel):
127 m.Canceled = true
128 }
129 }
130
131 return m, nil
132}
133
134// View implements tea.Model.
135func (m CleanupModel) View() string {
136 t := m.theme
137
138 // Build header
139 header := t.Title.
140 Foreground(t.Error).
141 Render("Clear History")
142
143 // Build range list
144 var rows []string
145 for i, r := range m.Ranges {
146 cursor := cursorEmpty
147 style := t.ListItem
148 if i == m.Selected {
149 cursor = cursorSelected
150 style = t.ListItemSelected
151 }
152
153 label := style.Render(r.Label)
154 desc := t.Subtle.Render(r.Description)
155
156 row := lipgloss.JoinHorizontal(
157 lipgloss.Left,
158 t.Highlight.Render(cursor),
159 lipgloss.JoinVertical(lipgloss.Left, label, desc),
160 )
161 rows = append(rows, row)
162 }
163
164 list := lipgloss.JoinVertical(lipgloss.Left, rows...)
165
166 // Warning for "All time"
167 var warning string
168 if m.Ranges[m.Selected].Range == CleanupRangeAll {
169 warning = t.ErrorStyle.Render("⚠ This will delete ALL history entries!")
170 }
171
172 // Help text
173 help := t.Subtle.Render("↑/↓ to select • enter to confirm • esc to cancel")
174
175 // Combine
176 var content string
177 if warning != "" {
178 content = lipgloss.JoinVertical(
179 lipgloss.Left,
180 header,
181 "",
182 list,
183 "",
184 warning,
185 "",
186 help,
187 )
188 } else {
189 content = lipgloss.JoinVertical(
190 lipgloss.Left,
191 header,
192 "",
193 list,
194 "",
195 help,
196 )
197 }
198
199 return t.Box.Render(content)
200}
201
202// Done returns true if the modal is complete.
203func (m CleanupModel) Done() bool {
204 return m.Confirmed || m.Canceled
205}
206
207// SelectedRange returns the selected cleanup range.
208func (m CleanupModel) SelectedRange() CleanupRange {
209 if m.Selected >= 0 && m.Selected < len(m.Ranges) {
210 return m.Ranges[m.Selected].Range
211 }
212 return CleanupRangeAll
213}
214
215// CleanupResultMsg is sent when cleanup modal completes.
216type CleanupResultMsg struct {
217 Confirmed bool
218 Range CleanupRange
219}