From f5963c088f135b51d5627f7b499506069ad416d5 Mon Sep 17 00:00:00 2001 From: atticl Date: Tue, 11 Nov 2025 10:07:15 +0100 Subject: [PATCH] push --- .todos/test.json | 23 ++++ go.mod | 3 + main.go | 334 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 360 insertions(+) create mode 100644 .todos/test.json create mode 100644 go.mod create mode 100644 main.go diff --git a/.todos/test.json b/.todos/test.json new file mode 100644 index 0000000..9da22d1 --- /dev/null +++ b/.todos/test.json @@ -0,0 +1,23 @@ +{ + "todos": [ + { + "id": 1, + "task": "Test 1", + "completed": true, + "created_at": "2025-11-11T10:06:26.232036+01:00" + }, + { + "id": 2, + "task": "Test 2", + "completed": false, + "created_at": "2025-11-11T10:06:33.4405526+01:00" + }, + { + "id": 3, + "task": "Test 3", + "completed": true, + "created_at": "2025-11-11T10:06:36.4225131+01:00" + } + ], + "next_id": 4 +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..84d671f --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module cli-todo + +go 1.24.5 diff --git a/main.go b/main.go new file mode 100644 index 0000000..9e64259 --- /dev/null +++ b/main.go @@ -0,0 +1,334 @@ +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) + } +}