cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
29
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 745 lines 20 kB view raw
1package ui 2 3import ( 4 "bytes" 5 "context" 6 "strings" 7 "testing" 8 "time" 9 10 "github.com/charmbracelet/bubbles/help" 11 "github.com/charmbracelet/bubbles/textinput" 12 tea "github.com/charmbracelet/bubbletea" 13 "github.com/stormlightlabs/noteleaf/internal/models" 14 "github.com/stormlightlabs/noteleaf/internal/repo" 15) 16 17type mockTaskRepo struct { 18 tasks map[int64]*models.Task 19 updated []*models.Task 20} 21 22func (m *mockTaskRepo) List(ctx context.Context, opts repo.TaskListOptions) ([]*models.Task, error) { 23 var result []*models.Task 24 for _, task := range m.tasks { 25 result = append(result, task) 26 } 27 return result, nil 28} 29 30func (m *mockTaskRepo) Update(ctx context.Context, task *models.Task) error { 31 m.updated = append(m.updated, task) 32 if existing, exists := m.tasks[task.ID]; exists { 33 *existing = *task 34 } 35 return nil 36} 37 38func createTestTaskEditModel(task *models.Task) taskEditModel { 39 now := time.Now() 40 if task.Entry.IsZero() { 41 task.Entry = now 42 } 43 if task.Modified.IsZero() { 44 task.Modified = now 45 } 46 47 repo := &mockTaskRepo{tasks: map[int64]*models.Task{task.ID: task}} 48 49 model := taskEditModel{ 50 task: task, 51 originalTask: task, 52 repo: repo, 53 opts: TaskEditOptions{Output: &bytes.Buffer{}, Width: 80, Height: 24}, 54 keys: taskEditKeys, 55 help: help.New(), 56 57 mode: fieldNavigation, 58 currentField: 0, 59 priorityMode: priorityModeText, 60 61 fields: []string{"Description", "Status", "Priority", "Project"}, 62 } 63 64 model.descInput = textinput.New() 65 model.descInput.SetValue(task.Description) 66 model.projectInput = textinput.New() 67 model.projectInput.SetValue(task.Project) 68 69 for i, status := range statusOptions { 70 if status == task.Status { 71 model.statusIndex = i 72 break 73 } 74 } 75 76 model.updatePriorityIndex() 77 78 return model 79} 80 81func TestTaskEditor(t *testing.T) { 82 t.Run("Creation", func(t *testing.T) { 83 task := &models.Task{ 84 ID: 1, 85 Description: "Test task", 86 Status: models.StatusTodo, 87 Priority: models.PriorityHigh, 88 Project: "test-project", 89 } 90 91 repo := &mockTaskRepo{tasks: map[int64]*models.Task{1: task}} 92 editor := NewTaskEditor(task, repo, TaskEditOptions{Width: 80, Height: 24}) 93 94 if editor.task != task { 95 t.Error("Task should be set correctly") 96 } 97 98 if editor.repo != repo { 99 t.Error("Repository should be set correctly") 100 } 101 102 if editor.opts.Width != 80 { 103 t.Errorf("Expected width 80, got %d", editor.opts.Width) 104 } 105 }) 106 107 t.Run("Default Options", func(t *testing.T) { 108 task := &models.Task{ID: 1} 109 repo := &mockTaskRepo{} 110 editor := NewTaskEditor(task, repo, TaskEditOptions{}) 111 112 if editor.opts.Width != 80 { 113 t.Errorf("Expected default width 80, got %d", editor.opts.Width) 114 } 115 116 if editor.opts.Height != 24 { 117 t.Errorf("Expected default height 24, got %d", editor.opts.Height) 118 } 119 }) 120} 121 122func TestTaskEditModel(t *testing.T) { 123 t.Run("Init", func(t *testing.T) { 124 task := &models.Task{ 125 ID: 1, 126 Description: "Test task", 127 Status: models.StatusInProgress, 128 Priority: models.PriorityMedium, 129 } 130 131 model := createTestTaskEditModel(task) 132 cmd := model.Init() 133 if cmd == nil { 134 t.Error("Init should return a command") 135 } 136 }) 137 138 t.Run("Field Navigation", func(t *testing.T) { 139 task := &models.Task{ID: 1, Description: "Test task", Status: models.StatusTodo} 140 model := createTestTaskEditModel(task) 141 142 if model.currentField != 0 { 143 t.Errorf("Expected initial field 0, got %d", model.currentField) 144 } 145 146 msg := tea.KeyMsg{Type: tea.KeyDown} 147 updatedModel, _ := model.Update(msg) 148 model = updatedModel.(taskEditModel) 149 150 if model.currentField != 1 { 151 t.Errorf("Expected field 1 after down, got %d", model.currentField) 152 } 153 154 msg = tea.KeyMsg{Type: tea.KeyUp} 155 updatedModel, _ = model.Update(msg) 156 model = updatedModel.(taskEditModel) 157 158 if model.currentField != 0 { 159 t.Errorf("Expected field 0 after up, got %d", model.currentField) 160 } 161 }) 162 163 t.Run("Status Picker", func(t *testing.T) { 164 task := &models.Task{ID: 1, Description: "Test task", Status: models.StatusTodo} 165 model := createTestTaskEditModel(task) 166 model.currentField = 1 167 168 msg := tea.KeyMsg{Type: tea.KeyEnter} 169 updatedModel, _ := model.Update(msg) 170 model = updatedModel.(taskEditModel) 171 172 if model.mode != statusPicker { 173 t.Errorf("Expected statusPicker mode, got %d", model.mode) 174 } 175 176 msg = tea.KeyMsg{Type: tea.KeyDown} 177 updatedModel, _ = model.Update(msg) 178 model = updatedModel.(taskEditModel) 179 180 if model.statusIndex != 1 { 181 t.Errorf("Expected status index 1, got %d", model.statusIndex) 182 } 183 184 msg = tea.KeyMsg{Type: tea.KeyEnter} 185 updatedModel, _ = model.Update(msg) 186 model = updatedModel.(taskEditModel) 187 188 if model.task.Status != statusOptions[1] { 189 t.Errorf("Expected status %s, got %s", statusOptions[1], model.task.Status) 190 } 191 192 if model.mode != fieldNavigation { 193 t.Errorf("Expected fieldNavigation mode after selection, got %d", model.mode) 194 } 195 }) 196 197 t.Run("Priority Picker", func(t *testing.T) { 198 task := &models.Task{ID: 1, Description: "Test task", Priority: ""} 199 model := createTestTaskEditModel(task) 200 model.currentField = 2 201 202 msg := tea.KeyMsg{Type: tea.KeyEnter} 203 updatedModel, _ := model.Update(msg) 204 model = updatedModel.(taskEditModel) 205 206 if model.mode != priorityPicker { 207 t.Errorf("Expected priorityPicker mode, got %d", model.mode) 208 } 209 210 msg = tea.KeyMsg{Type: tea.KeyDown} 211 updatedModel, _ = model.Update(msg) 212 model = updatedModel.(taskEditModel) 213 214 if model.priorityIndex != 1 { 215 t.Errorf("Expected priority index 1, got %d", model.priorityIndex) 216 } 217 218 msg = tea.KeyMsg{Type: tea.KeyEnter} 219 updatedModel, _ = model.Update(msg) 220 model = updatedModel.(taskEditModel) 221 222 expectedPriority := textPriorityOptions[1] 223 if model.task.Priority != expectedPriority { 224 t.Errorf("Expected priority %s, got %s", expectedPriority, model.task.Priority) 225 } 226 }) 227 228 t.Run("Priority Mode Switch", func(t *testing.T) { 229 task := &models.Task{ID: 1, Priority: models.PriorityHigh} 230 model := createTestTaskEditModel(task) 231 model.mode = priorityPicker 232 233 if model.priorityMode != priorityModeText { 234 t.Errorf("Expected text priority mode initially, got %d", model.priorityMode) 235 } 236 237 msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'m'}} 238 updatedModel, _ := model.Update(msg) 239 model = updatedModel.(taskEditModel) 240 241 if model.priorityMode != priorityModeNumeric { 242 t.Errorf("Expected numeric priority mode, got %d", model.priorityMode) 243 } 244 245 msg = tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'m'}} 246 updatedModel, _ = model.Update(msg) 247 model = updatedModel.(taskEditModel) 248 249 if model.priorityMode != priorityModeLegacy { 250 t.Errorf("Expected legacy priority mode, got %d", model.priorityMode) 251 } 252 253 msg = tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'m'}} 254 updatedModel, _ = model.Update(msg) 255 model = updatedModel.(taskEditModel) 256 257 if model.priorityMode != priorityModeText { 258 t.Errorf("Expected text priority mode after full cycle, got %d", model.priorityMode) 259 } 260 }) 261 262 t.Run("TextInput", func(t *testing.T) { 263 task := &models.Task{ID: 1, Description: "Original description", Project: "original-project"} 264 265 model := createTestTaskEditModel(task) 266 model.currentField = 0 267 268 msg := tea.KeyMsg{Type: tea.KeyEnter} 269 updatedModel, _ := model.Update(msg) 270 model = updatedModel.(taskEditModel) 271 272 if model.mode != textInput { 273 t.Errorf("Expected textInput mode, got %d", model.mode) 274 } 275 276 msg = tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'X'}} 277 updatedModel, _ = model.Update(msg) 278 model = updatedModel.(taskEditModel) 279 280 msg = tea.KeyMsg{Type: tea.KeyEnter} 281 updatedModel, _ = model.Update(msg) 282 model = updatedModel.(taskEditModel) 283 284 if model.mode != fieldNavigation { 285 t.Errorf("Expected fieldNavigation mode after text input, got %d", model.mode) 286 } 287 288 expected := "Original descriptionX" 289 if model.task.Description != expected { 290 t.Errorf("Expected description %s, got %s", expected, model.task.Description) 291 } 292 }) 293 294 t.Run("Help", func(t *testing.T) { 295 task := &models.Task{ID: 1} 296 model := createTestTaskEditModel(task) 297 298 msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'?'}} 299 updatedModel, _ := model.Update(msg) 300 model = updatedModel.(taskEditModel) 301 302 if !model.showingHelp { 303 t.Error("Expected help to be shown") 304 } 305 306 msg = tea.KeyMsg{Type: tea.KeyEsc} 307 updatedModel, _ = model.Update(msg) 308 model = updatedModel.(taskEditModel) 309 310 if model.showingHelp { 311 t.Error("Expected help to be hidden") 312 } 313 }) 314 315 t.Run("Save", func(t *testing.T) { 316 task := &models.Task{ID: 1} 317 model := createTestTaskEditModel(task) 318 msg := tea.KeyMsg{Type: tea.KeyCtrlS} 319 updatedModel, cmd := model.Update(msg) 320 model = updatedModel.(taskEditModel) 321 322 if !model.saved { 323 t.Error("Expected saved flag to be set") 324 } 325 326 if cmd == nil { 327 t.Error("Expected quit command after save") 328 } 329 }) 330 331 t.Run("Cancel", func(t *testing.T) { 332 task := &models.Task{ID: 1} 333 model := createTestTaskEditModel(task) 334 msg := tea.KeyMsg{Type: tea.KeyCtrlC} 335 updatedModel, cmd := model.Update(msg) 336 model = updatedModel.(taskEditModel) 337 338 if !model.cancelled { 339 t.Error("Expected cancelled flag to be set") 340 } 341 342 if cmd == nil { 343 t.Error("Expected quit command after cancel") 344 } 345 }) 346 347 t.Run("View", func(t *testing.T) { 348 task := &models.Task{ 349 ID: 1, 350 Description: "Test task", 351 Status: models.StatusTodo, 352 Priority: models.PriorityHigh, 353 Project: "test-project", 354 } 355 356 model := createTestTaskEditModel(task) 357 view := model.View() 358 359 if !strings.Contains(view, "Edit Task") { 360 t.Error("View should contain title") 361 } 362 363 if !strings.Contains(view, "Test task") { 364 t.Error("View should contain task description") 365 } 366 367 if !strings.Contains(view, "test-project") { 368 t.Error("View should contain project") 369 } 370 }) 371 372 t.Run("Status Picker View", func(t *testing.T) { 373 task := &models.Task{ID: 1, Status: models.StatusTodo} 374 model := createTestTaskEditModel(task) 375 model.mode = statusPicker 376 377 view := model.View() 378 379 if !strings.Contains(view, "Select Status:") { 380 t.Error("Status picker should show selection prompt") 381 } 382 383 for _, status := range statusOptions { 384 if !strings.Contains(view, status) { 385 t.Errorf("Status picker should contain %s", status) 386 } 387 } 388 }) 389 390 t.Run("Priority Picker View", func(t *testing.T) { 391 task := &models.Task{ID: 1, Priority: ""} 392 model := createTestTaskEditModel(task) 393 model.mode = priorityPicker 394 model.priorityMode = priorityModeText 395 396 view := model.View() 397 398 if !strings.Contains(view, "Select Priority") { 399 t.Error("Priority picker should show selection prompt") 400 } 401 402 if !strings.Contains(view, "Text") { 403 t.Error("Priority picker should show current mode") 404 } 405 }) 406 407 t.Run("KeyBindings", func(t *testing.T) { 408 keyMap := taskEditKeys 409 410 if keyMap.Up.Keys()[0] != "up" { 411 t.Error("Up key binding should be defined") 412 } 413 414 if keyMap.StatusEdit.Keys()[0] != "s" { 415 t.Error("Status edit key binding should be 's'") 416 } 417 418 if keyMap.Priority.Keys()[0] != "p" { 419 t.Error("Priority key binding should be 'p'") 420 } 421 422 if keyMap.PriorityMode.Keys()[0] != "m" { 423 t.Error("Priority mode key binding should be 'm'") 424 } 425 }) 426} 427 428func TestUpdatePriorityIndex(t *testing.T) { 429 testCases := []struct { 430 priority string 431 mode priorityMode 432 expectedIdx int 433 }{ 434 {models.PriorityHigh, priorityModeText, 3}, 435 {models.PriorityMedium, priorityModeText, 2}, 436 {models.PriorityLow, priorityModeText, 1}, 437 {"", priorityModeText, 0}, 438 {"3", priorityModeNumeric, 3}, 439 {"A", priorityModeLegacy, 1}, 440 {"unknown", priorityModeText, 0}, 441 } 442 443 for _, tc := range testCases { 444 task := &models.Task{ID: 1, Priority: tc.priority} 445 model := createTestTaskEditModel(task) 446 model.priorityMode = tc.mode 447 model.updatePriorityIndex() 448 449 if model.priorityIndex != tc.expectedIdx { 450 t.Errorf("Priority %s in mode %d should have index %d, got %d", 451 tc.priority, tc.mode, tc.expectedIdx, model.priorityIndex) 452 } 453 } 454} 455 456func TestRenderStatusField(t *testing.T) { 457 task := &models.Task{ID: 1, Status: models.StatusInProgress} 458 model := createTestTaskEditModel(task) 459 460 result := model.renderStatusField() 461 if !strings.Contains(result, models.StatusInProgress) { 462 t.Error("Status field should contain the status") 463 } 464 465 model.mode = statusPicker 466 result = model.renderStatusField() 467 if !strings.Contains(result, models.StatusTodo) || !strings.Contains(result, models.StatusDone) { 468 t.Error("Status picker should show status legend") 469 } 470} 471 472func TestRenderPriorityField(t *testing.T) { 473 task := &models.Task{ID: 1, Priority: models.PriorityMedium} 474 model := createTestTaskEditModel(task) 475 result := model.renderPriorityField() 476 if !strings.Contains(result, models.PriorityMedium) { 477 t.Error("Priority field should contain the priority") 478 } 479 480 model.mode = priorityPicker 481 model.priorityMode = priorityModeNumeric 482 result = model.renderPriorityField() 483 if !strings.Contains(result, "Numeric") { 484 t.Error("Priority picker should show current mode") 485 } 486} 487 488// TestUncoveredPriorityModes tests all priority mode switch cases 489func TestUncoveredPriorityModes(t *testing.T) { 490 t.Run("Priority Mode Display Strings", func(t *testing.T) { 491 task := &models.Task{ID: 1, Priority: models.PriorityHigh} 492 model := createTestTaskEditModel(task) 493 model.mode = priorityPicker 494 495 // Test numeric mode 496 model.priorityMode = priorityModeNumeric 497 result := model.renderPriorityPicker() 498 if !strings.Contains(result, "Numeric") { 499 t.Error("Priority picker should show Numeric mode") 500 } 501 502 // Test legacy mode 503 model.priorityMode = priorityModeLegacy 504 result = model.renderPriorityPicker() 505 if !strings.Contains(result, "Legacy") { 506 t.Error("Priority picker should show Legacy mode") 507 } 508 }) 509 510 t.Run("Priority Display Type Switches", func(t *testing.T) { 511 tests := []struct { 512 name string 513 priority string 514 expectedType string 515 }{ 516 {"Numeric Priority", "3", "numeric"}, 517 {"Legacy Priority A", "A", "legacy"}, 518 {"Legacy Priority E", "E", "legacy"}, 519 } 520 521 for _, tt := range tests { 522 t.Run(tt.name, func(t *testing.T) { 523 task := &models.Task{ID: 1, Priority: tt.priority} 524 525 displayType := GetPriorityDisplayType(task.Priority) 526 if displayType != tt.expectedType { 527 t.Errorf("Expected display type %s for priority %s, got %s (task:%v)", tt.expectedType, tt.priority, displayType, task) 528 } 529 }) 530 } 531 }) 532} 533 534func TestUncoveredKeyboardNavigation(t *testing.T) { 535 t.Run("Status Edit Key Binding", func(t *testing.T) { 536 task := &models.Task{ID: 1, Status: models.StatusTodo} 537 model := createTestTaskEditModel(task) 538 539 msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'s'}} 540 updatedModel, _ := model.Update(msg) 541 model = updatedModel.(taskEditModel) 542 543 if model.mode != statusPicker { 544 t.Error("Expected status picker mode when 's' key is pressed") 545 } 546 }) 547 548 t.Run("Priority Edit Key Binding", func(t *testing.T) { 549 task := &models.Task{ID: 1} 550 model := createTestTaskEditModel(task) 551 552 msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'p'}} 553 updatedModel, _ := model.Update(msg) 554 model = updatedModel.(taskEditModel) 555 556 if model.mode != priorityPicker { 557 t.Error("Expected priority picker mode when 'p' key is pressed") 558 } 559 }) 560 561 t.Run("Window Resize Handling", func(t *testing.T) { 562 task := &models.Task{ID: 1} 563 model := createTestTaskEditModel(task) 564 565 msg := tea.WindowSizeMsg{Width: 120, Height: 40} 566 updatedModel, _ := model.Update(msg) 567 model = updatedModel.(taskEditModel) 568 569 if model.opts.Width != 120 { 570 t.Errorf("Expected width to be updated to 120, got %d", model.opts.Width) 571 } 572 }) 573 574 t.Run("Escape Key in Status Picker", func(t *testing.T) { 575 task := &models.Task{ID: 1} 576 model := createTestTaskEditModel(task) 577 model.mode = statusPicker 578 579 msg := tea.KeyMsg{Type: tea.KeyEsc} 580 updatedModel, _ := model.Update(msg) 581 model = updatedModel.(taskEditModel) 582 583 if model.mode != fieldNavigation { 584 t.Error("Expected to return to field navigation when escape is pressed in status picker") 585 } 586 }) 587 588 t.Run("Escape Key in Priority Picker", func(t *testing.T) { 589 task := &models.Task{ID: 1} 590 model := createTestTaskEditModel(task) 591 model.mode = priorityPicker 592 593 msg := tea.KeyMsg{Type: tea.KeyEsc} 594 updatedModel, _ := model.Update(msg) 595 model = updatedModel.(taskEditModel) 596 597 if model.mode != fieldNavigation { 598 t.Error("Expected to return to field navigation when escape is pressed in priority picker") 599 } 600 }) 601 602 t.Run("Navigation Keys in Status Picker", func(t *testing.T) { 603 task := &models.Task{ID: 1} 604 model := createTestTaskEditModel(task) 605 model.mode = statusPicker 606 model.statusIndex = 0 607 608 // Test down/right navigation 609 msg := tea.KeyMsg{Type: tea.KeyDown} 610 updatedModel, _ := model.Update(msg) 611 model = updatedModel.(taskEditModel) 612 613 if model.statusIndex != 1 { 614 t.Errorf("Expected status index to be 1, got %d", model.statusIndex) 615 } 616 617 // Test up/left navigation 618 msg = tea.KeyMsg{Type: tea.KeyUp} 619 updatedModel, _ = model.Update(msg) 620 model = updatedModel.(taskEditModel) 621 622 if model.statusIndex != 0 { 623 t.Errorf("Expected status index to be 0, got %d", model.statusIndex) 624 } 625 }) 626 627 t.Run("Navigation Keys in Priority Picker", func(t *testing.T) { 628 task := &models.Task{ID: 1} 629 model := createTestTaskEditModel(task) 630 model.mode = priorityPicker 631 model.priorityIndex = 0 632 633 // Test down/right navigation 634 msg := tea.KeyMsg{Type: tea.KeyDown} 635 updatedModel, _ := model.Update(msg) 636 model = updatedModel.(taskEditModel) 637 638 if model.priorityIndex != 1 { 639 t.Errorf("Expected priority index to be 1, got %d", model.priorityIndex) 640 } 641 642 // Test up/left navigation 643 msg = tea.KeyMsg{Type: tea.KeyUp} 644 updatedModel, _ = model.Update(msg) 645 model = updatedModel.(taskEditModel) 646 647 if model.priorityIndex != 0 { 648 t.Errorf("Expected priority index to be 0, got %d", model.priorityIndex) 649 } 650 }) 651} 652 653func TestUncoveredFieldSwitches(t *testing.T) { 654 t.Run("Field Entry Switch Cases", func(t *testing.T) { 655 task := &models.Task{ID: 1, Description: "Test", Project: "TestProject"} 656 model := createTestTaskEditModel(task) 657 658 model.currentField = 3 659 model.mode = textInput 660 661 msg := tea.KeyMsg{Type: tea.KeyEnter} 662 updatedModel, _ := model.Update(msg) 663 model = updatedModel.(taskEditModel) 664 665 if model.mode != fieldNavigation { 666 t.Error("Expected to return to field navigation after entering project field") 667 } 668 }) 669 670 t.Run("Field Update Switch Cases", func(t *testing.T) { 671 task := &models.Task{ID: 1, Description: "Test", Project: "TestProject"} 672 model := createTestTaskEditModel(task) 673 model.currentField = 3 674 model.mode = textInput 675 676 model.projectInput.SetValue("Updated Project") 677 678 msg := tea.KeyMsg{Type: tea.KeyEnter} 679 updatedModel, _ := model.Update(msg) 680 model = updatedModel.(taskEditModel) 681 682 if model.task.Project != "Updated Project" { 683 t.Errorf("Expected project to be updated to 'Updated Project', got %s", model.task.Project) 684 } 685 }) 686} 687 688func TestTaskFieldAccessors(t *testing.T) { 689 t.Run("Task Field Value Extraction", func(t *testing.T) { 690 now := time.Now() 691 task := &models.Task{ 692 ID: 1, 693 Description: "Test", 694 Tags: []string{"tag1", "tag2"}, 695 Due: &now, 696 Entry: now, 697 Start: &now, 698 End: &now, 699 } 700 701 tests := []struct { 702 field string 703 expected any 704 }{ 705 {"due", task.Due}, 706 {"entry", task.Entry}, 707 {"start", task.Start}, 708 {"end", task.End}, 709 } 710 711 for _, tt := range tests { 712 t.Run(tt.field, func(t *testing.T) { 713 result := getTaskFieldValue(task, tt.field) 714 if result != tt.expected { 715 t.Errorf("Expected %v for field %s, got %v", tt.expected, tt.field, result) 716 } 717 }) 718 } 719 }) 720} 721 722func getTaskFieldValue(task *models.Task, field string) any { 723 switch field { 724 case "description": 725 return task.Description 726 case "status": 727 return task.Status 728 case "priority": 729 return task.Priority 730 case "project": 731 return task.Project 732 case "tags": 733 return task.Tags 734 case "due": 735 return task.Due 736 case "entry": 737 return task.Entry 738 case "start": 739 return task.Start 740 case "end": 741 return task.End 742 default: 743 return nil 744 } 745}