package main import ( "bufio" "encoding/json" "fmt" "os" "path/filepath" "strconv" "strings" "time" ) type Todo struct { ID int `json:"id"` Task string `json:"task"` Completed bool `json:"completed"` CreatedAt time.Time `json:"created_at"` } type TodoList struct { Todos []Todo `json:"todos"` NextID int `json:"next_id"` } const ( dataDir = ".todos" reset = "\033[0m" red = "\033[31m" green = "\033[32m" yellow = "\033[33m" blue = "\033[34m" magenta = "\033[35m" cyan = "\033[36m" bold = "\033[1m" ) var currentProject string func main() { ensureDataDir() reader := bufio.NewReader(os.Stdin) currentProject = selectProject(reader) list := loadTodos(currentProject) for { printHeader() printMenu() input, _ := reader.ReadString('\n') choice := strings.TrimSpace(input) switch choice { case "1": addTodo(&list, reader) case "2": listTodos(&list) case "3": completeTodo(&list, reader) case "4": deleteTodo(&list, reader) case "5": currentProject = selectProject(reader) list = loadTodos(currentProject) case "6": listProjects() case "7": deleteProject(reader) case "8": fmt.Println(cyan + "Goodbye! ✨" + reset) return default: fmt.Println(red + "Invalid option" + reset) } } } func printHeader() { fmt.Println("\n" + strings.Repeat("=", 50)) fmt.Printf("%s%s✓ Todo Manager%s\n", bold, cyan, reset) fmt.Printf("%sProject:%s %s%s%s\n", yellow, reset, green, currentProject, reset) fmt.Println(strings.Repeat("=", 50)) } func printMenu() { fmt.Println("\n" + bold + "Actions:" + reset) fmt.Println(cyan + "1." + reset + " Add todo") fmt.Println(cyan + "2." + reset + " List todos") fmt.Println(cyan + "3." + reset + " Complete todo") fmt.Println(cyan + "4." + reset + " Delete todo") fmt.Println(magenta + "5." + reset + " Switch project") fmt.Println(magenta + "6." + reset + " List all projects") fmt.Println(red + "7." + reset + " Delete project") fmt.Println(yellow + "8." + reset + " Exit") fmt.Print("\n" + bold + "Choose: " + reset) } func selectProject(reader *bufio.Reader) string { projects := getProjects() fmt.Println("\n" + bold + cyan + "Available Projects:" + reset) if len(projects) == 0 { fmt.Println(yellow + "No projects found. Create a new one!" + reset) } else { for i, p := range projects { fmt.Printf("%s%d.%s %s\n", cyan, i+1, reset, p) } } fmt.Print("\n" + bold + "Enter project name (or number): " + reset) input, _ := reader.ReadString('\n') input = strings.TrimSpace(input) if input == "" { return "default" } // Check if input is a number if num, err := strconv.Atoi(input); err == nil && num > 0 && num <= len(projects) { return projects[num-1] } return input } func addTodo(list *TodoList, reader *bufio.Reader) { fmt.Print(bold + "Enter task: " + reset) task, _ := reader.ReadString('\n') task = strings.TrimSpace(task) if task == "" { fmt.Println(red + "Task cannot be empty" + reset) return } todo := Todo{ ID: list.NextID, Task: task, Completed: false, CreatedAt: time.Now(), } list.Todos = append(list.Todos, todo) list.NextID++ saveTodos(list, currentProject) fmt.Println(green + "✓ Todo added!" + reset) } func listTodos(list *TodoList) { if len(list.Todos) == 0 { fmt.Println(yellow + "\nNo todos yet! Add one to get started." + reset) return } completed := 0 for _, todo := range list.Todos { if todo.Completed { completed++ } } fmt.Printf("\n%s%sYour Todos%s (%d total, %d completed)\n", bold, cyan, reset, len(list.Todos), completed) fmt.Println(strings.Repeat("-", 50)) for _, todo := range list.Todos { status := red + "[ ]" + reset taskColor := reset if todo.Completed { status = green + "[✓]" + reset taskColor = "\033[90m" // Gray } dateStr := todo.CreatedAt.Format("Jan 02") fmt.Printf("%s%d.%s %s %s%s%s %s(%s)%s\n", cyan, todo.ID, reset, status, taskColor, todo.Task, reset, yellow, dateStr, reset) } } func completeTodo(list *TodoList, reader *bufio.Reader) { listTodos(list) if len(list.Todos) == 0 { return } fmt.Print("\n" + bold + "Enter todo ID to complete: " + reset) input, _ := reader.ReadString('\n') id, err := strconv.Atoi(strings.TrimSpace(input)) if err != nil { fmt.Println(red + "Invalid ID" + reset) return } for i := range list.Todos { if list.Todos[i].ID == id { list.Todos[i].Completed = true saveTodos(list, currentProject) fmt.Println(green + "✓ Todo completed!" + reset) return } } fmt.Println(red + "Todo not found" + reset) } func deleteTodo(list *TodoList, reader *bufio.Reader) { listTodos(list) if len(list.Todos) == 0 { return } fmt.Print("\n" + bold + "Enter todo ID to delete: " + reset) input, _ := reader.ReadString('\n') id, err := strconv.Atoi(strings.TrimSpace(input)) if err != nil { fmt.Println(red + "Invalid ID" + reset) return } for i := range list.Todos { if list.Todos[i].ID == id { list.Todos = append(list.Todos[:i], list.Todos[i+1:]...) saveTodos(list, currentProject) fmt.Println(green + "✓ Todo deleted!" + reset) return } } fmt.Println(red + "Todo not found" + reset) } func listProjects() { projects := getProjects() fmt.Println("\n" + bold + cyan + "All Projects:" + reset) if len(projects) == 0 { fmt.Println(yellow + "No projects found" + reset) return } for _, p := range projects { if p == currentProject { fmt.Printf("%s→ %s%s (current)\n", green, p, reset) } else { fmt.Printf(" %s\n", p) } list := loadTodos(p) total := len(list.Todos) completed := 0 for _, todo := range list.Todos { if todo.Completed { completed++ } } fmt.Printf(" %s%d todos (%d completed)%s\n", yellow, total, completed, reset) } } func deleteProject(reader *bufio.Reader) { projects := getProjects() if len(projects) == 0 { fmt.Println(yellow + "No projects to delete" + reset) return } listProjects() fmt.Print("\n" + bold + red + "Enter project name to delete: " + reset) input, _ := reader.ReadString('\n') name := strings.TrimSpace(input) if name == "" { return } if name == currentProject { fmt.Println(red + "Cannot delete current project. Switch first." + reset) return } filename := filepath.Join(dataDir, name+".json") if err := os.Remove(filename); err != nil { fmt.Println(red + "Error deleting project" + reset) return } fmt.Println(green + "✓ Project deleted!" + reset) } func ensureDataDir() { if err := os.MkdirAll(dataDir, 0755); err != nil { fmt.Println(red + "Error creating data directory" + reset) os.Exit(1) } } func getProjects() []string { files, err := os.ReadDir(dataDir) if err != nil { return []string{} } var projects []string for _, file := range files { if !file.IsDir() && strings.HasSuffix(file.Name(), ".json") { projects = append(projects, strings.TrimSuffix(file.Name(), ".json")) } } return projects } func loadTodos(project string) TodoList { filename := filepath.Join(dataDir, project+".json") data, err := os.ReadFile(filename) if err != nil { return TodoList{Todos: []Todo{}, NextID: 1} } var list TodoList if err := json.Unmarshal(data, &list); err != nil { return TodoList{Todos: []Todo{}, NextID: 1} } return list } func saveTodos(list *TodoList, project string) { filename := filepath.Join(dataDir, project+".json") data, err := json.MarshalIndent(list, "", " ") if err != nil { fmt.Println(red + "Error saving todos" + reset) return } if err := os.WriteFile(filename, data, 0644); err != nil { fmt.Println(red + "Error writing file" + reset) } }