cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists ๐Ÿƒ
charm leaflet readability golang

refactor: pull out task helpers in handlers pkg

+218 -209
+212
internal/handlers/task_helpers.go
··· 1 + package handlers 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "strings" 7 + "time" 8 + 9 + "github.com/stormlightlabs/noteleaf/internal/models" 10 + "golang.org/x/text/feature/plural" 11 + "golang.org/x/text/language" 12 + ) 13 + 14 + // ParsedTaskData holds extracted metadata from a task description 15 + type ParsedTaskData struct { 16 + Description string 17 + Project string 18 + Context string 19 + Tags []string 20 + Due string 21 + Recur string 22 + Until string 23 + ParentUUID string 24 + DependsOn []string 25 + } 26 + 27 + // parseDescription extracts inline metadata from description text 28 + // Supports: +project @context #tag due:YYYY-MM-DD recur:RULE until:DATE parent:UUID depends:UUID1,UUID2 29 + func parseDescription(text string) *ParsedTaskData { 30 + parsed := &ParsedTaskData{Tags: []string{}, DependsOn: []string{}} 31 + words := strings.Fields(text) 32 + 33 + var descWords []string 34 + for _, word := range words { 35 + switch { 36 + case strings.HasPrefix(word, "+"): 37 + parsed.Project = strings.TrimPrefix(word, "+") 38 + case strings.HasPrefix(word, "@"): 39 + parsed.Context = strings.TrimPrefix(word, "@") 40 + case strings.HasPrefix(word, "#"): 41 + parsed.Tags = append(parsed.Tags, strings.TrimPrefix(word, "#")) 42 + case strings.HasPrefix(word, "due:"): 43 + parsed.Due = strings.TrimPrefix(word, "due:") 44 + case strings.HasPrefix(word, "recur:"): 45 + parsed.Recur = strings.TrimPrefix(word, "recur:") 46 + case strings.HasPrefix(word, "until:"): 47 + parsed.Until = strings.TrimPrefix(word, "until:") 48 + case strings.HasPrefix(word, "parent:"): 49 + parsed.ParentUUID = strings.TrimPrefix(word, "parent:") 50 + case strings.HasPrefix(word, "depends:"): 51 + deps := strings.TrimPrefix(word, "depends:") 52 + parsed.DependsOn = strings.Split(deps, ",") 53 + default: 54 + descWords = append(descWords, word) 55 + } 56 + } 57 + 58 + parsed.Description = strings.Join(descWords, " ") 59 + return parsed 60 + } 61 + 62 + func removeString(slice []string, item string) []string { 63 + var result []string 64 + for _, s := range slice { 65 + if s != item { 66 + result = append(result, s) 67 + } 68 + } 69 + return result 70 + } 71 + 72 + func pluralize(count int) string { 73 + rule := plural.Cardinal.MatchPlural(language.English, count, 0, 0, 0, 0) 74 + switch rule { 75 + case plural.One: 76 + return "" 77 + default: 78 + return "s" 79 + } 80 + } 81 + 82 + func formatDuration(d time.Duration) string { 83 + if d < time.Minute { 84 + return fmt.Sprintf("%.0fs", d.Seconds()) 85 + } 86 + if d < time.Hour { 87 + return fmt.Sprintf("%.0fm", d.Minutes()) 88 + } 89 + hours := d.Hours() 90 + if hours < 24 { 91 + return fmt.Sprintf("%.1fh", hours) 92 + } 93 + days := int(hours / 24) 94 + if remainingHours := hours - float64(days*24); remainingHours == 0 { 95 + return fmt.Sprintf("%dd", days) 96 + } else { 97 + return fmt.Sprintf("%dd %.1fh", days, remainingHours) 98 + } 99 + } 100 + 101 + func printTask(task *models.Task) { 102 + fmt.Printf("[%d] %s", task.ID, task.Description) 103 + 104 + if task.Status != "pending" { 105 + fmt.Printf(" (%s)", task.Status) 106 + } 107 + 108 + if task.Priority != "" { 109 + fmt.Printf(" [%s]", task.Priority) 110 + } 111 + 112 + if task.Project != "" { 113 + fmt.Printf(" +%s", task.Project) 114 + } 115 + 116 + if task.Context != "" { 117 + fmt.Printf(" @%s", task.Context) 118 + } 119 + 120 + if len(task.Tags) > 0 { 121 + fmt.Printf(" #%s", strings.Join(task.Tags, " #")) 122 + } 123 + 124 + if task.Due != nil { 125 + fmt.Printf(" (due: %s)", task.Due.Format("2006-01-02")) 126 + } 127 + 128 + if task.Recur != "" { 129 + fmt.Printf(" \u21bb") 130 + } 131 + 132 + if len(task.DependsOn) > 0 { 133 + fmt.Printf(" \u2937%d", len(task.DependsOn)) 134 + } 135 + 136 + fmt.Println() 137 + } 138 + 139 + func printTaskDetail(task *models.Task, noMetadata bool) { 140 + fmt.Printf("Task ID: %d\n", task.ID) 141 + fmt.Printf("UUID: %s\n", task.UUID) 142 + fmt.Printf("Description: %s\n", task.Description) 143 + fmt.Printf("Status: %s\n", task.Status) 144 + 145 + if task.Priority != "" { 146 + fmt.Printf("Priority: %s\n", task.Priority) 147 + } 148 + 149 + if task.Project != "" { 150 + fmt.Printf("Project: %s\n", task.Project) 151 + } 152 + 153 + if task.Context != "" { 154 + fmt.Printf("Context: %s\n", task.Context) 155 + } 156 + 157 + if len(task.Tags) > 0 { 158 + fmt.Printf("Tags: %s\n", strings.Join(task.Tags, ", ")) 159 + } 160 + 161 + if task.Due != nil { 162 + fmt.Printf("Due: %s\n", task.Due.Format("2006-01-02 15:04")) 163 + } 164 + 165 + if task.Recur != "" { 166 + fmt.Printf("Recurrence: %s\n", task.Recur) 167 + } 168 + 169 + if task.Until != nil { 170 + fmt.Printf("Recur Until: %s\n", task.Until.Format("2006-01-02")) 171 + } 172 + 173 + if task.ParentUUID != nil { 174 + fmt.Printf("Parent Task: %s\n", *task.ParentUUID) 175 + } 176 + 177 + if len(task.DependsOn) > 0 { 178 + fmt.Printf("Depends On:\n") 179 + for _, dep := range task.DependsOn { 180 + fmt.Printf(" - %s\n", dep) 181 + } 182 + } 183 + 184 + if !noMetadata { 185 + fmt.Printf("Created: %s\n", task.Entry.Format("2006-01-02 15:04")) 186 + fmt.Printf("Modified: %s\n", task.Modified.Format("2006-01-02 15:04")) 187 + 188 + if task.Start != nil { 189 + fmt.Printf("Started: %s\n", task.Start.Format("2006-01-02 15:04")) 190 + } 191 + 192 + if task.End != nil { 193 + fmt.Printf("Completed: %s\n", task.End.Format("2006-01-02 15:04")) 194 + } 195 + } 196 + 197 + if len(task.Annotations) > 0 { 198 + fmt.Printf("Annotations:\n") 199 + for _, annotation := range task.Annotations { 200 + fmt.Printf(" - %s\n", annotation) 201 + } 202 + } 203 + } 204 + 205 + func printTaskJSON(task *models.Task) error { 206 + if data, err := json.MarshalIndent(task, "", " "); err != nil { 207 + return fmt.Errorf("failed to marshal task to JSON: %w", err) 208 + } else { 209 + fmt.Println(string(data)) 210 + return nil 211 + } 212 + }
+4 -207
internal/handlers/tasks.go
··· 2 2 3 3 import ( 4 4 "context" 5 - "encoding/json" 6 5 "fmt" 7 6 "os" 8 7 "slices" ··· 15 14 "github.com/stormlightlabs/noteleaf/internal/repo" 16 15 "github.com/stormlightlabs/noteleaf/internal/store" 17 16 "github.com/stormlightlabs/noteleaf/internal/ui" 18 - "golang.org/x/text/feature/plural" 19 - "golang.org/x/text/language" 20 17 ) 21 18 22 19 // TaskHandler handles all task-related commands ··· 188 185 189 186 fmt.Printf("Found %d task(s):\n\n", len(tasks)) 190 187 for _, task := range tasks { 191 - h.printTask(task) 188 + printTask(task) 192 189 } 193 190 194 191 return nil ··· 380 377 } 381 378 382 379 if jsonOutput { 383 - return h.printTaskJSON(task) 380 + return printTaskJSON(task) 384 381 } 385 382 386 383 if format == "brief" { 387 - h.printTask(task) 384 + printTask(task) 388 385 } else { 389 - h.printTaskDetail(task, noMetadata) 386 + printTaskDetail(task, noMetadata) 390 387 } 391 388 return nil 392 389 } ··· 736 733 return tagTable.Browse(ctx) 737 734 } 738 735 739 - func (h *TaskHandler) printTask(task *models.Task) { 740 - fmt.Printf("[%d] %s", task.ID, task.Description) 741 - 742 - if task.Status != "pending" { 743 - fmt.Printf(" (%s)", task.Status) 744 - } 745 - 746 - if task.Priority != "" { 747 - fmt.Printf(" [%s]", task.Priority) 748 - } 749 - 750 - if task.Project != "" { 751 - fmt.Printf(" +%s", task.Project) 752 - } 753 - 754 - if task.Context != "" { 755 - fmt.Printf(" @%s", task.Context) 756 - } 757 - 758 - if len(task.Tags) > 0 { 759 - fmt.Printf(" #%s", strings.Join(task.Tags, " #")) 760 - } 761 - 762 - if task.Due != nil { 763 - fmt.Printf(" (due: %s)", task.Due.Format("2006-01-02")) 764 - } 765 - 766 - if task.Recur != "" { 767 - fmt.Printf(" \u21bb") 768 - } 769 - 770 - if len(task.DependsOn) > 0 { 771 - fmt.Printf(" \u2937%d", len(task.DependsOn)) 772 - } 773 - 774 - fmt.Println() 775 - } 776 - 777 - func (h *TaskHandler) printTaskDetail(task *models.Task, noMetadata bool) { 778 - fmt.Printf("Task ID: %d\n", task.ID) 779 - fmt.Printf("UUID: %s\n", task.UUID) 780 - fmt.Printf("Description: %s\n", task.Description) 781 - fmt.Printf("Status: %s\n", task.Status) 782 - 783 - if task.Priority != "" { 784 - fmt.Printf("Priority: %s\n", task.Priority) 785 - } 786 - 787 - if task.Project != "" { 788 - fmt.Printf("Project: %s\n", task.Project) 789 - } 790 - 791 - if task.Context != "" { 792 - fmt.Printf("Context: %s\n", task.Context) 793 - } 794 - 795 - if len(task.Tags) > 0 { 796 - fmt.Printf("Tags: %s\n", strings.Join(task.Tags, ", ")) 797 - } 798 - 799 - if task.Due != nil { 800 - fmt.Printf("Due: %s\n", task.Due.Format("2006-01-02 15:04")) 801 - } 802 - 803 - if task.Recur != "" { 804 - fmt.Printf("Recurrence: %s\n", task.Recur) 805 - } 806 - 807 - if task.Until != nil { 808 - fmt.Printf("Recur Until: %s\n", task.Until.Format("2006-01-02")) 809 - } 810 - 811 - if task.ParentUUID != nil { 812 - fmt.Printf("Parent Task: %s\n", *task.ParentUUID) 813 - } 814 - 815 - if len(task.DependsOn) > 0 { 816 - fmt.Printf("Depends On:\n") 817 - for _, dep := range task.DependsOn { 818 - fmt.Printf(" - %s\n", dep) 819 - } 820 - } 821 - 822 - if !noMetadata { 823 - fmt.Printf("Created: %s\n", task.Entry.Format("2006-01-02 15:04")) 824 - fmt.Printf("Modified: %s\n", task.Modified.Format("2006-01-02 15:04")) 825 - 826 - if task.Start != nil { 827 - fmt.Printf("Started: %s\n", task.Start.Format("2006-01-02 15:04")) 828 - } 829 - 830 - if task.End != nil { 831 - fmt.Printf("Completed: %s\n", task.End.Format("2006-01-02 15:04")) 832 - } 833 - } 834 - 835 - if len(task.Annotations) > 0 { 836 - fmt.Printf("Annotations:\n") 837 - for _, annotation := range task.Annotations { 838 - fmt.Printf(" - %s\n", annotation) 839 - } 840 - } 841 - } 842 - 843 - func (h *TaskHandler) printTaskJSON(task *models.Task) error { 844 - jsonData, err := json.MarshalIndent(task, "", " ") 845 - if err != nil { 846 - return fmt.Errorf("failed to marshal task to JSON: %w", err) 847 - } 848 - fmt.Println(string(jsonData)) 849 - return nil 850 - } 851 - 852 736 // SetRecur sets the recurrence rule for a task 853 737 func (h *TaskHandler) SetRecur(ctx context.Context, taskID, rule, until string) error { 854 738 var task *models.Task ··· 1074 958 1075 959 return nil 1076 960 } 1077 - 1078 - // ParsedTaskData holds extracted metadata from a task description 1079 - type ParsedTaskData struct { 1080 - Description string 1081 - Project string 1082 - Context string 1083 - Tags []string 1084 - Due string 1085 - Recur string 1086 - Until string 1087 - ParentUUID string 1088 - DependsOn []string 1089 - } 1090 - 1091 - // parseDescription extracts inline metadata from description text 1092 - // Supports: +project @context #tag due:YYYY-MM-DD recur:RULE until:DATE parent:UUID depends:UUID1,UUID2 1093 - func parseDescription(text string) *ParsedTaskData { 1094 - parsed := &ParsedTaskData{Tags: []string{}, DependsOn: []string{}} 1095 - words := strings.Fields(text) 1096 - 1097 - var descWords []string 1098 - for _, word := range words { 1099 - switch { 1100 - case strings.HasPrefix(word, "+"): 1101 - parsed.Project = strings.TrimPrefix(word, "+") 1102 - case strings.HasPrefix(word, "@"): 1103 - parsed.Context = strings.TrimPrefix(word, "@") 1104 - case strings.HasPrefix(word, "#"): 1105 - parsed.Tags = append(parsed.Tags, strings.TrimPrefix(word, "#")) 1106 - case strings.HasPrefix(word, "due:"): 1107 - parsed.Due = strings.TrimPrefix(word, "due:") 1108 - case strings.HasPrefix(word, "recur:"): 1109 - parsed.Recur = strings.TrimPrefix(word, "recur:") 1110 - case strings.HasPrefix(word, "until:"): 1111 - parsed.Until = strings.TrimPrefix(word, "until:") 1112 - case strings.HasPrefix(word, "parent:"): 1113 - parsed.ParentUUID = strings.TrimPrefix(word, "parent:") 1114 - case strings.HasPrefix(word, "depends:"): 1115 - deps := strings.TrimPrefix(word, "depends:") 1116 - parsed.DependsOn = strings.Split(deps, ",") 1117 - default: 1118 - descWords = append(descWords, word) 1119 - } 1120 - } 1121 - 1122 - parsed.Description = strings.Join(descWords, " ") 1123 - return parsed 1124 - } 1125 - 1126 - func removeString(slice []string, item string) []string { 1127 - var result []string 1128 - for _, s := range slice { 1129 - if s != item { 1130 - result = append(result, s) 1131 - } 1132 - } 1133 - return result 1134 - } 1135 - 1136 - func pluralize(count int) string { 1137 - rule := plural.Cardinal.MatchPlural(language.English, count, 0, 0, 0, 0) 1138 - switch rule { 1139 - case plural.One: 1140 - return "" 1141 - default: 1142 - return "s" 1143 - } 1144 - } 1145 - 1146 - func formatDuration(d time.Duration) string { 1147 - if d < time.Minute { 1148 - return fmt.Sprintf("%.0fs", d.Seconds()) 1149 - } 1150 - if d < time.Hour { 1151 - return fmt.Sprintf("%.0fm", d.Minutes()) 1152 - } 1153 - hours := d.Hours() 1154 - if hours < 24 { 1155 - return fmt.Sprintf("%.1fh", hours) 1156 - } 1157 - days := int(hours / 24) 1158 - if remainingHours := hours - float64(days*24); remainingHours == 0 { 1159 - return fmt.Sprintf("%dd", days) 1160 - } else { 1161 - return fmt.Sprintf("%dd %.1fh", days, remainingHours) 1162 - } 1163 - }
+2 -2
internal/handlers/tasks_test.go
··· 811 811 } 812 812 }() 813 813 814 - handler.printTask(task) 814 + printTask(task) 815 815 }) 816 816 817 817 t.Run("printTaskDetail doesn't panic", func(t *testing.T) { ··· 821 821 } 822 822 }() 823 823 824 - handler.printTaskDetail(task, false) 824 + printTaskDetail(task, false) 825 825 }) 826 826 }) 827 827