This commit is contained in:
2025-11-11 10:07:15 +01:00
parent b53ed856ba
commit f5963c088f
3 changed files with 360 additions and 0 deletions

23
.todos/test.json Normal file
View File

@@ -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
}

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module cli-todo
go 1.24.5

334
main.go Normal file
View File

@@ -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)
}
}