335 lines
7.5 KiB
Go
335 lines
7.5 KiB
Go
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)
|
|
}
|
|
}
|