v0.33.2 57f21d6

cleanup.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close