@@ -92,6 +92,12 @@ type Model struct {
9292
9393 plugHost * plugins.Host
9494 plugCh chan struct {}
95+
96+ // Animation state for strike action
97+ animatingTaskID string
98+ animationStarted time.Time
99+ animationDuration time.Duration
100+ animationReverse bool // true if uncompleting (reverse strike), false if completing
95101}
96102
97103func New (ctx context.Context , cfg config.Config , repo * storage.Repository ) (tea.Model , error ) {
@@ -322,6 +328,31 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
322328 case taskDeletedMsg :
323329 return m , tea .Batch (m .loadTagsCmd (), m .loadTasksCmd (), m .loadAllTasksCmd (), m .syncIfEnabledCmd ())
324330
331+ case strikeAnimationTickMsg :
332+ if m .animatingTaskID != x .TaskID {
333+ return m , nil
334+ }
335+ elapsed := time .Since (m .animationStarted )
336+ if elapsed >= m .animationDuration {
337+ // Animation complete, update the task
338+ m .animatingTaskID = ""
339+ var taskToUpdate core.Task
340+ for _ , t := range m .all {
341+ if t .ID == x .TaskID {
342+ taskToUpdate = t
343+ break
344+ }
345+ }
346+ newStatus := core .StatusDone
347+ if taskToUpdate .Status == core .StatusDone {
348+ newStatus = core .StatusTodo
349+ }
350+ patch := core.TaskPatch {Status : & newStatus }
351+ return m , m .updateTaskCmd (x .TaskID , patch )
352+ }
353+ // Continue animation
354+ return m , m .strikeAnimationTickCmd (x .TaskID )
355+
325356 case openTaskMsg :
326357 m .det .SetTask (x .Task )
327358 m .mode = ModeDetail
@@ -463,6 +494,14 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
463494 if t , ok := m .list .Selected (); ok {
464495 return m , m .fetchOpenTaskCmd (t .ID )
465496 }
497+ case keymapMatch (m .km .ToggleStrike , km ):
498+ if t , ok := m .list .Selected (); ok {
499+ m .animatingTaskID = t .ID
500+ m .animationStarted = time .Now ()
501+ m .animationDuration = 400 * time .Millisecond
502+ m .animationReverse = (t .Status == core .StatusDone )
503+ return m , m .strikeAnimationTickCmd (t .ID )
504+ }
466505 }
467506 }
468507
@@ -474,6 +513,14 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
474513 if keymapMatch (m .km .EditTask , km ) {
475514 return m , m .fetchOpenEditCmd (m .det .Task ().ID )
476515 }
516+ if keymapMatch (m .km .ToggleStrike , km ) {
517+ t := m .det .Task ()
518+ m .animatingTaskID = t .ID
519+ m .animationStarted = time .Now ()
520+ m .animationDuration = 400 * time .Millisecond
521+ m .animationReverse = (t .Status == core .StatusDone )
522+ return m , m .strikeAnimationTickCmd (t .ID )
523+ }
477524 }
478525
479526 if m .mode == ModePluginUninstall {
@@ -582,6 +629,11 @@ func (m *Model) renderMainUI() string {
582629 availableHeight = 0
583630 }
584631
632+ // Sync animation state to tasklist
633+ if m .animatingTaskID != "" {
634+ m .list .SetAnimation (m .animatingTaskID , m .animationStarted , m .animationDuration , m .animationReverse )
635+ }
636+
585637 var body string
586638 switch m .mode {
587639 case ModeList , ModeConfirmDelete :
@@ -723,7 +775,7 @@ func (m *Model) renderFooter() string {
723775 left = " " + m .s .Muted .Render (
724776 fk (m .km .Palette )+ " " + styles .IconPalette + " • " +
725777 fk (m .km .NewTask )+ " " + styles .IconNew + " • " +
726- "g "+ styles .IconSync + " • " +
778+ fk ( m . km . ToggleStrike ) + " "+ styles .IconStrike + " • " +
727779 fk (m .km .DeleteTask )+ " " + styles .IconDelete + " • " +
728780 fk (m .km .Help )+ " " + styles .IconHelp + " • " +
729781 fk (m .km .ViewInbox )+ "-" + fk (m .km .ViewPriority )+ " " + styles .IconView + " " ,
@@ -859,6 +911,13 @@ func (m *Model) deleteTaskCmd(id string) tea.Cmd {
859911 }
860912}
861913
914+ func (m * Model ) strikeAnimationTickCmd (taskID string ) tea.Cmd {
915+ return func () tea.Msg {
916+ time .Sleep (16 * time .Millisecond ) // ~60 FPS
917+ return strikeAnimationTickMsg {TaskID : taskID }
918+ }
919+ }
920+
862921func (m * Model ) fetchOpenTaskCmd (id string ) tea.Cmd {
863922 return func () tea.Msg {
864923 t , err := m .repo .GetTask (m .ctx , id )
0 commit comments