1package model
2
3import (
4 "context"
5 "fmt"
6
7 tea "github.com/charmbracelet/bubbletea"
8 "github.com/charmbracelet/lipgloss"
9
10 "github.com/bnema/dumber/internal/application/usecase"
11 "github.com/bnema/dumber/internal/cli/styles"
12 "github.com/bnema/dumber/internal/domain/entity"
13)
14
15// PurgeModel wraps styles.PurgeModel for standalone CLI use.
16type PurgeModel struct {
17 selector styles.PurgeModel
18 purgeUC *usecase.PurgeDataUseCase
19 listSessionsUC *usecase.ListSessionsUseCase
20 deleteSessionUC *usecase.DeleteSessionUseCase
21
22 loading bool
23 loadingSessions bool
24 purging bool
25 done bool
26
27 // Track loading state for parallel loading
28 targetsLoaded bool
29 sessionsLoaded bool
30 targets []entity.PurgeTarget
31 sessions []entity.SessionPurgeItem
32 sessionsSize int64
33
34 results *usecase.PurgeOutput
35 sessionResults []sessionPurgeResult
36 info string
37 err error
38
39 theme *styles.Theme
40 ctx context.Context
41}
42
43// sessionPurgeResult tracks the result of deleting a session.
44type sessionPurgeResult struct {
45 SessionID entity.SessionID
46 Success bool
47 Error error
48}
49
50// PurgeModelConfig contains optional dependencies for session purging.
51type PurgeModelConfig struct {
52 ListSessionsUC *usecase.ListSessionsUseCase
53 DeleteSessionUC *usecase.DeleteSessionUseCase
54}
55
56// NewPurgeModel creates a new purge command model.
57func NewPurgeModel(ctx context.Context, theme *styles.Theme, purgeUC *usecase.PurgeDataUseCase, cfg PurgeModelConfig) PurgeModel {
58 m := PurgeModel{
59 theme: theme,
60 purgeUC: purgeUC,
61 listSessionsUC: cfg.ListSessionsUC,
62 deleteSessionUC: cfg.DeleteSessionUC,
63 loading: true,
64 loadingSessions: cfg.ListSessionsUC != nil,
65 ctx: ctx,
66 }
67 return m
68}
69
70type purgeTargetsLoadedMsg struct {
71 targets []entity.PurgeTarget
72 err error
73}
74
75type purgeSessionsLoadedMsg struct {
76 sessions []entity.SessionPurgeItem
77 size int64
78 err error
79}
80
81type purgeCompleteMsg struct {
82 targetResults []entity.PurgeResult
83 sessionResults []sessionPurgeResult
84 err error
85}
86
87// Init implements tea.Model.
88func (m PurgeModel) Init() tea.Cmd {
89 cmds := []tea.Cmd{m.loadTargets()}
90 if m.listSessionsUC != nil {
91 cmds = append(cmds, m.loadSessions())
92 }
93 return tea.Batch(cmds...)
94}
95
96func (m PurgeModel) loadTargets() tea.Cmd {
97 return func() tea.Msg {
98 targets, err := m.purgeUC.GetPurgeTargets(context.Background())
99 return purgeTargetsLoadedMsg{targets: targets, err: err}
100 }
101}
102
103func (m PurgeModel) loadSessions() tea.Cmd {
104 return func() tea.Msg {
105 output, err := m.listSessionsUC.GetPurgeableSessions(context.Background())
106 if err != nil {
107 return purgeSessionsLoadedMsg{err: err}
108 }
109 return purgeSessionsLoadedMsg{
110 sessions: output.Sessions,
111 size: output.TotalSize,
112 }
113 }
114}
115
116// Update implements tea.Model.
117func (m PurgeModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
118 switch msg := msg.(type) {
119 case purgeTargetsLoadedMsg:
120 return m.handleTargetsLoaded(msg), nil
121 case purgeSessionsLoadedMsg:
122 return m.handleSessionsLoaded(msg), nil
123 case purgeCompleteMsg:
124 return m.handlePurgeComplete(msg), nil
125 case tea.KeyMsg:
126 if m.done {
127 return m, tea.Quit
128 }
129 if m.isBlocked() {
130 return m, nil
131 }
132 // Fall through to updateSelector
133 }
134
135 if m.isBlocked() {
136 return m, nil
137 }
138
139 return m.updateSelector(msg)
140}
141
142func (m PurgeModel) handleTargetsLoaded(msg purgeTargetsLoadedMsg) PurgeModel {
143 if msg.err != nil {
144 m.err = msg.err
145 m.done = true
146 m.loading = false
147 return m
148 }
149 m.targetsLoaded = true
150 m.targets = msg.targets
151 return m.checkLoadingComplete()
152}
153
154func (m PurgeModel) handleSessionsLoaded(msg purgeSessionsLoadedMsg) PurgeModel {
155 if msg.err != nil {
156 // Session loading failure is non-fatal, continue without sessions
157 m.sessionsLoaded = true
158 m.loadingSessions = false
159 return m.checkLoadingComplete()
160 }
161 m.sessionsLoaded = true
162 m.sessions = msg.sessions
163 m.sessionsSize = msg.size
164 return m.checkLoadingComplete()
165}
166
167func (m PurgeModel) handlePurgeComplete(msg purgeCompleteMsg) PurgeModel {
168 m.purging = false
169 m.done = true
170 if msg.targetResults != nil {
171 m.results = &usecase.PurgeOutput{Results: msg.targetResults}
172 for _, r := range msg.targetResults {
173 if r.Success {
174 m.results.SuccessCount++
175 } else {
176 m.results.FailureCount++
177 }
178 }
179 }
180 m.sessionResults = msg.sessionResults
181 if msg.err != nil {
182 m.err = msg.err
183 }
184 return m
185}
186
187func (m PurgeModel) isBlocked() bool {
188 return m.loading || m.loadingSessions || m.purging
189}
190
191func (m PurgeModel) updateSelector(msg tea.Msg) (tea.Model, tea.Cmd) {
192 selector, cmd := m.selector.Update(msg)
193 m.selector = selector
194
195 if !m.selector.Done() {
196 return m, cmd
197 }
198
199 if m.selector.Canceled {
200 return m, tea.Quit
201 }
202
203 if m.selector.Confirmed {
204 targetTypes := m.selector.SelectedTypes()
205 sessionIDs := m.selector.SelectedSessionIDs()
206 if len(targetTypes) == 0 && len(sessionIDs) == 0 {
207 m.done = true
208 m.info = "Nothing selected"
209 return m, nil
210 }
211 m.purging = true
212 return m, m.performPurge(targetTypes, sessionIDs)
213 }
214
215 return m, cmd
216}
217
218func (m PurgeModel) checkLoadingComplete() PurgeModel {
219 // If sessions aren't being loaded, only wait for targets
220 needSessions := m.listSessionsUC != nil
221 if m.targetsLoaded && (!needSessions || m.sessionsLoaded) {
222 m.loading = false
223 m.loadingSessions = false
224 m.selector = styles.NewPurgeWithSessions(m.theme, m.targets, m.sessions, m.sessionsSize)
225 }
226 return m
227}
228
229func (m PurgeModel) performPurge(targetTypes []entity.PurgeTargetType, sessionIDs []entity.SessionID) tea.Cmd {
230 return func() tea.Msg {
231 var targetResults []entity.PurgeResult
232 var sessionResults []sessionPurgeResult
233 var lastErr error
234
235 // Delete sessions first (if any selected and Data not selected)
236 if m.deleteSessionUC != nil {
237 for _, sid := range sessionIDs {
238 err := m.deleteSessionUC.Execute(m.ctx, usecase.DeleteSessionInput{
239 SessionID: sid,
240 CurrentSessionID: "", // CLI has no current session
241 })
242 sessionResults = append(sessionResults, sessionPurgeResult{
243 SessionID: sid,
244 Success: err == nil,
245 Error: err,
246 })
247 if err != nil {
248 lastErr = err
249 }
250 }
251 }
252
253 // Then delete target directories
254 if len(targetTypes) > 0 {
255 out, err := m.purgeUC.Execute(m.ctx, usecase.PurgeInput{TargetTypes: targetTypes})
256 if out != nil {
257 targetResults = out.Results
258 }
259 if err != nil {
260 lastErr = err
261 }
262 }
263
264 return purgeCompleteMsg{
265 targetResults: targetResults,
266 sessionResults: sessionResults,
267 err: lastErr,
268 }
269 }
270}
271
272// View implements tea.Model.
273func (m PurgeModel) View() string {
274 t := m.theme
275
276 if m.loading || m.loadingSessions {
277 spinner := styles.NewLoading(t, "Scanning purge targets...")
278 return t.Box.Render(spinner.View())
279 }
280
281 if m.purging {
282 spinner := styles.NewLoading(t, "Purging...")
283 return t.Box.Render(spinner.View())
284 }
285
286 if m.done {
287 if m.info != "" {
288 content := lipgloss.JoinVertical(
289 lipgloss.Left,
290 t.Subtle.Render(m.info),
291 "",
292 t.Subtle.Render("Press any key to exit"),
293 )
294 return t.Box.Render(content)
295 }
296
297 if m.err != nil && m.results == nil && len(m.sessionResults) == 0 {
298 content := lipgloss.JoinVertical(
299 lipgloss.Left,
300 t.ErrorStyle.Render("Error: "+m.err.Error()),
301 "",
302 t.Subtle.Render("Press any key to exit"),
303 )
304 return t.Box.Render(content)
305 }
306
307 return m.renderResults()
308 }
309
310 return m.selector.View()
311}
312
313func (m PurgeModel) renderResults() string {
314 t := m.theme
315 contentLines := []string{t.Title.Render("Purge complete")}
316
317 successCount := 0
318 failureCount := 0
319
320 // Target results
321 if m.results != nil {
322 for _, r := range m.results.Results {
323 if r.Success {
324 contentLines = append(contentLines, fmt.Sprintf("%s %s", t.SuccessStyle.Render(styles.IconCheck), r.Target.Path))
325 successCount++
326 } else {
327 contentLines = append(contentLines, fmt.Sprintf("%s %s: %v", t.ErrorStyle.Render(styles.IconX), r.Target.Path, r.Error))
328 failureCount++
329 }
330 }
331 }
332
333 // Session results
334 for _, r := range m.sessionResults {
335 if r.Success {
336 contentLines = append(contentLines, fmt.Sprintf("%s Session %s", t.SuccessStyle.Render(styles.IconCheck), r.SessionID))
337 successCount++
338 } else {
339 contentLines = append(contentLines, fmt.Sprintf("%s Session %s: %v", t.ErrorStyle.Render(styles.IconX), r.SessionID, r.Error))
340 failureCount++
341 }
342 }
343
344 contentLines = append(
345 contentLines,
346 "",
347 t.Subtle.Render(fmt.Sprintf("%d succeeded, %d failed", successCount, failureCount)),
348 "",
349 t.Subtle.Render("Press any key to exit"),
350 )
351
352 return t.Box.Render(lipgloss.JoinVertical(lipgloss.Left, contentLines...))
353}
354
355var _ tea.Model = (*PurgeModel)(nil)