diff --git a/doxa-todo/MANUAL.md b/doxa-todo/MANUAL.md index 777e9cd..15098a4 100644 --- a/doxa-todo/MANUAL.md +++ b/doxa-todo/MANUAL.md @@ -33,6 +33,7 @@ The main interface displays: - **/: Search** - Search todos by title, description, or tags - **b: Tags** - Browse all tags - **c: Filter** - Filter todos by category +- **t: Toggle Complete** - Toggle completion status of the selected todo - **l: EN/CS** - Toggle language between English and Czech - **q: Quit** - Exit the application @@ -138,6 +139,27 @@ The exported file contains: 3. Press `y` to confirm deletion 4. Press `n` or `Esc` to cancel +## Completed Todos + +The application allows you to mark todos as completed, helping you track your progress. + +### Marking Todos as Completed + +1. Navigate to the todo you want to mark as completed/incomplete +2. Press `t` to toggle its completion status +3. Completed todos will: + - Be displayed with strikethrough and gray color + - Be automatically moved to the end of the list + - Be saved with the new status + +### Completed Todos Features + +- **Automatic Sorting**: Completed todos are always displayed last in the list +- **Visualization**: Completed todos are shown with strikethrough and gray color for easy identification +- **Persistent Storage**: Completion status is saved in the JSON file along with the todo +- **Filtering**: Completed todos are included in all filters and searches +- **Export**: When exporting a completed todo, its status is included in the exported data + ## Keyboard Shortcuts Summary ### Navigation @@ -154,6 +176,7 @@ The exported file contains: - `/`: Search - `b`: Browse tags - `c`: Filter by category +- `t`: Toggle todo completion status - `l`: Toggle language - `+`: Increase priority (when adding/editing) - `Tab`: Switch between fields diff --git a/doxa-todo/MANUAL_CZ.md b/doxa-todo/MANUAL_CZ.md index 49fefa2..d17bcf7 100644 --- a/doxa-todo/MANUAL_CZ.md +++ b/doxa-todo/MANUAL_CZ.md @@ -33,6 +33,7 @@ Hlavní rozhraní zobrazuje: - **/: Hledat** - Vyhledat úkoly podle názvu, popisu nebo štítků - **b: Tagy** - Procházet všechny štítky - **c: Filtr** - Filtr úkolů podle kategorie +- **t: Splněno** - Přepnout stav splnění vybraného úkolu - **l: EN/CS** - Přepnout jazyk mezi angličtinou a češtinou - **q: Konec** - Ukončit aplikaci @@ -138,6 +139,27 @@ Exportovaný soubor obsahuje: 3. Stiskněte `y` pro potvrzení smazání 4. Stiskněte `n` nebo `Esc` pro zrušení +## Splněné úkoly + +Aplikace umožňuje označovat úkoly jako splněné, což vám pomůže sledovat váš pokrok. + +### Označování úkolů jako splněných + +1. Navigujte k úkolu, který chcete označit jako splněný/nesplněný +2. Stiskněte `t` pro přepnutí stavu +3. Splněné úkoly budou: + - Zobrazeny přeškrtlé a šedě + - Automaticky přesunuty na konec seznamu + - Uloženy s novým stavem + +### Vlastnosti splněných úkolů + +- **Automatické řazení**: Splněné úkoly jsou vždy zobrazeny jako poslední v seznamu +- **Vizualizace**: Splněné úkoly jsou přeškrtlé a zobrazují se šedou barvou pro snadnou identifikaci +- **Trvalé uložení**: Stav splnění je uložen v JSON souboru spolu s úkolem +- **Filtrace**: Splněné úkoly jsou zahrnuty ve všech filtrech a vyhledáváních +- **Export**: Při exportu splněného úkolu je jeho stav zahrnut v exportovaných datech + ## Přehled klávesových zkratek ### Navigace @@ -154,6 +176,7 @@ Exportovaný soubor obsahuje: - `/`: Vyhledat - `b`: Procházet štítky - `c`: Filtr podle kategorie +- `t`: Přepnout stav splnění úkolu - `l`: Přepnout jazyk - `+`: Zvýšit prioritu (při přidávání/úpravě) - `Tab`: Přepínat mezi poli diff --git a/doxa-todo/README.md b/doxa-todo/README.md index dce3976..c8da385 100644 --- a/doxa-todo/README.md +++ b/doxa-todo/README.md @@ -12,6 +12,7 @@ - **Persistent Storage**: Todos are saved to `~/.doxa-todo/doxa_todo.json` - **Search & Filter**: Search by text, browse by tags, filter by category - **Export**: Export individual todos to text files +- **Completed Todos**: Mark todos as completed/incomplete with automatic sorting to the end of the list ## Installation @@ -69,6 +70,7 @@ export PATH=$PATH:~/.local/bin | `b` | Browse tags | | `c` | Filter by category | | `l` | Toggle language (Czech/English) | +| `t` | Toggle todo completion status | | `+` | Increase priority (when adding/editing) | | `↑/↓` or `j/k` | Navigate through list | | `Tab` | Switch between fields | @@ -83,6 +85,7 @@ Each todo has: - **Tags**: Comma-separated list - **Created At**: Automatic timestamp - **Modified At**: Automatic timestamp on edits +- **Completed**: Completion status (toggle with `t` key) ### Storage @@ -96,6 +99,14 @@ Exported todos are saved to: ~/doxa-todo-export/ ``` +## Completed Todos + +The application supports marking todos as completed: +- **Toggle Status**: Select a todo and press `t` to toggle between completed/incomplete +- **Visualization**: Completed todos are displayed with strikethrough and gray color +- **Automatic Sorting**: Completed todos are always displayed at the end of the list (after incomplete ones) +- **Persistent Storage**: Completion status is saved with the todo + ## Configuration The application automatically creates the configuration directory and file on first run. diff --git a/doxa-todo/README_CZ.md b/doxa-todo/README_CZ.md index 528d818..5c140b6 100644 --- a/doxa-todo/README_CZ.md +++ b/doxa-todo/README_CZ.md @@ -12,6 +12,7 @@ - **Trvalé uložení**: Úkoly se ukládají do `~/.doxa-todo/doxa_todo.json` - **Vyhledávání a filtrace**: Vyhledávání podle textu, procházení podle štítků, filtrace podle kategorie - **Export**: Export jednotlivých úkolů do textových souborů +- **Splněné úkoly**: Označování úkolů jako splněné/nesplněné s automatickým řazením na konec seznamu ## Instalace @@ -69,6 +70,7 @@ export PATH=$PATH:~/.local/bin | `b` | Procházet štítky | | `c` | Filtr podle kategorie | | `l` | Přepnout jazyk (Čeština/Angličtina) | +| `t` | Přepnout stav splnění úkolu | | `+` | Zvýšit prioritu (při přidávání/úpravě) | | `↑/↓` nebo `j/k` | Procházet seznamem | | `Tab` | Přepínat mezi poli | @@ -83,6 +85,7 @@ Každý úkol má: - **Štítky**: Seznam oddělený čárkami - **Vytvořeno**: Automatické časové razítko - **Upraveno**: Automatické časové razítko při úpravách +- **Splněno**: Stav splnění (přepíná se klávesou `t`) ### Uložení @@ -96,6 +99,14 @@ Exportované úkoly jsou uloženy v: ~/doxa-todo-export/ ``` +## Splněné úkoly + +Aplikace podporuje označování úkolů jako splněné: +- **Přepnutí stavu**: Vyberte úkol a stiskněte `t` pro přepnutí mezi splněným/nesplněným +- **Vizualizace**: Splněné úkoly jsou zobrazovány přeškrtlé a šedě +- **Automatické řazení**: Splněné úkoly jsou vždy zobrazeny na konci seznamu (za nesplněnými) +- **Trvalé uložení**: Stav splnění se ukládá spolu s úkolem + ## Konfigurace Aplikace automaticky vytvoří konfigurační adresář a soubor při prvním spuštění. diff --git a/doxa-todo/translations.go b/doxa-todo/translations.go index afe1f26..f34ea0d 100644 --- a/doxa-todo/translations.go +++ b/doxa-todo/translations.go @@ -11,6 +11,7 @@ type Translation struct { MenuSearch string MenuBrowse string MenuFilter string + MenuToggleComplete string ListTitle string NoTodos string AddTitle string @@ -30,6 +31,8 @@ type Translation struct { StatusEdited string StatusDeleted string StatusExported string + StatusCompleted string + StatusUncompleted string ErrRequired string SearchTitle string BrowseTags string @@ -55,6 +58,7 @@ var Translations = map[string]Translation{ MenuSearch: "/: Hledat", MenuBrowse: "b: Tagy", MenuFilter: "c: Filtr", + MenuToggleComplete: "t: Splněno", ListTitle: "--- SEZNAM ÚKOLŮ ---", NoTodos: " (Prázdno...)", AddTitle: "PŘIDAT ÚKOL", @@ -68,12 +72,14 @@ var Translations = map[string]Translation{ PlaceholderDescription: "Popis (nepovinný)", PlaceholderTags: "Tagy (nepovinné, oddělené čárkou)", HelpForm: "[Tab] Další pole | [Enter] Uložit | [+] Zvýšit prioritu | [-] Změnit kategorii | [q] Zrušit", - HelpList: "↑/↓: Vybrat | Enter: Zobrazit | /: Hledat | b: Procházet tagy | c: Filtr kategorie | q: Konec", + HelpList: "↑/↓: Vybrat | Enter: Zobrazit | /: Hledat | b: Procházet tagy | c: Filtr kategorie | t: Splněno | q: Konec", HelpFormExtended: "[Tab] Další pole | [Enter] Uložit | [+] Priorita | [-] Kategorie | [q] Zrušit", StatusSaved: "Úkol uložen.", StatusEdited: "Změny uloženy.", StatusDeleted: "Úkol smazán.", StatusExported: "Úkol vyexportován.", + StatusCompleted: "Úkol označen jako splněný.", + StatusUncompleted: "Úkol označen jako nesplněný.", ErrRequired: "Chyba: Název je povinný!", SearchTitle: "HLEDAT: ", BrowseTags: "TAGY", @@ -97,6 +103,7 @@ var Translations = map[string]Translation{ MenuSearch: "/: Search", MenuBrowse: "b: Tags", MenuFilter: "c: Filter", + MenuToggleComplete: "t: Toggle Complete", ListTitle: "--- TODOS LIST ---", NoTodos: " (Empty...)", AddTitle: "ADD TODO", @@ -110,12 +117,14 @@ var Translations = map[string]Translation{ PlaceholderDescription: "Description (optional)", PlaceholderTags: "Tags (optional, comma separated)", HelpForm: "[Tab] Next field | [Enter] Save | [+] Priority | [-] Category | [q] Cancel", - HelpList: "↑/↓: Select | Enter: View | /: Search | b: Browse tags | c: Filter category | q: Quit", + HelpList: "↑/↓: Select | Enter: View | /: Search | b: Browse tags | c: Filter category | t: Toggle Complete | q: Quit", HelpFormExtended: "[Tab] Next field | [Enter] Save | [+] Increase priority | [-] Change category | [q] Cancel", StatusSaved: "Todo saved.", StatusEdited: "Changes saved.", StatusDeleted: "Todo deleted.", StatusExported: "Todo exported.", + StatusCompleted: "Todo marked as completed.", + StatusUncompleted: "Todo marked as incomplete.", ErrRequired: "Error: Title is required!", SearchTitle: "SEARCH: ", BrowseTags: "TAGS", diff --git a/doxa-todo/tui.go b/doxa-todo/tui.go index 12fad14..caaefb8 100644 --- a/doxa-todo/tui.go +++ b/doxa-todo/tui.go @@ -54,6 +54,10 @@ var ( categoryWork = lipgloss.NewStyle(). Foreground(lipgloss.Color("#9B59B6")) // Fialová + + completedStyle = lipgloss.NewStyle(). + Strikethrough(true). + Foreground(lipgloss.Color("#95A5A6")) // Šedá pro splněné úkoly ) // Priority levels @@ -116,6 +120,7 @@ type Todo struct { Tags []string `json:"tags"` CreatedAt time.Time `json:"created_at"` ModifiedAt time.Time `json:"modified_at"` + Completed bool `json:"completed"` } type model struct { @@ -161,6 +166,7 @@ func NewModel() model { newTodoPriority: PriorityMedium, newTodoCategory: CategoryPersonal, } + m.sortTodos() m.applyFilter() m.updatePlaceholders() return m @@ -170,6 +176,28 @@ func (m model) Init() tea.Cmd { return tea.Batch(textinput.Blink, tea.EnterAltScreen) } +func (m *model) sortTodos() { + // Sort todos: completed ones always last, then by ID (newest first) + for i := 0; i < len(m.todos)-1; i++ { + for j := i + 1; j < len(m.todos); j++ { + // If i is completed and j is not, swap + if m.todos[i].Completed && !m.todos[j].Completed { + m.todos[i], m.todos[j] = m.todos[j], m.todos[i] + } else if !m.todos[i].Completed && !m.todos[j].Completed { + // Both not completed, sort by ID (newest first) + if m.todos[i].ID < m.todos[j].ID { + m.todos[i], m.todos[j] = m.todos[j], m.todos[i] + } + } else if m.todos[i].Completed && m.todos[j].Completed { + // Both completed, sort by ID (newest first) + if m.todos[i].ID < m.todos[j].ID { + m.todos[i], m.todos[j] = m.todos[j], m.todos[i] + } + } + } + } +} + func (m *model) applyFilter() { if m.filterText == "" && m.currentCategoryFilter == -1 { m.filteredTodos = m.todos @@ -196,6 +224,25 @@ func (m *model) applyFilter() { } } } + // Sort filtered todos: completed ones always last + for i := 0; i < len(m.filteredTodos)-1; i++ { + for j := i + 1; j < len(m.filteredTodos); j++ { + // If i is completed and j is not, swap + if m.filteredTodos[i].Completed && !m.filteredTodos[j].Completed { + m.filteredTodos[i], m.filteredTodos[j] = m.filteredTodos[j], m.filteredTodos[i] + } else if !m.filteredTodos[i].Completed && !m.filteredTodos[j].Completed { + // Both not completed, sort by ID (newest first) + if m.filteredTodos[i].ID < m.filteredTodos[j].ID { + m.filteredTodos[i], m.filteredTodos[j] = m.filteredTodos[j], m.filteredTodos[i] + } + } else if m.filteredTodos[i].Completed && m.filteredTodos[j].Completed { + // Both completed, sort by ID (newest first) + if m.filteredTodos[i].ID < m.filteredTodos[j].ID { + m.filteredTodos[i], m.filteredTodos[j] = m.filteredTodos[j], m.filteredTodos[i] + } + } + } + } if m.cursor >= len(m.filteredTodos) { m.cursor = 0 if len(m.filteredTodos) > 0 { @@ -492,6 +539,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.selectedTodoID = m.filteredTodos[m.cursor].ID m.focusIndex = 0 } + case "t": + if m.state == StateViewTodos && len(m.filteredTodos) > 0 { + m.toggleTodoCompleted() + } case "+": if m.state == StateAddTodo || m.state == StateEditTodo { // Cycle priority up @@ -735,7 +786,12 @@ func (m model) View() string { if m.cursor == i { cursor = selectedStyle.Render("> ") } - listBuilder.WriteString(fmt.Sprintf("%s%s %s %s%s\n", cursor, priorityIcon, categoryIcon, t.Title, tagStr)) + + title := t.Title + if t.Completed { + title = completedStyle.Render(t.Title) + } + listBuilder.WriteString(fmt.Sprintf("%s%s %s %s%s\n", cursor, priorityIcon, categoryIcon, title, tagStr)) } } @@ -743,8 +799,8 @@ func (m model) View() string { m.viewport.SetContent(listBuilder.String()) } - b.WriteString(fmt.Sprintf("%s | %s | %s | %s | %s | %s | %s | %s\n\n", - tr.MenuAdd, tr.MenuEdit, tr.MenuExport, tr.MenuDelete, tr.MenuSearch, tr.MenuBrowse, tr.MenuFilter, tr.MenuLang)) + b.WriteString(fmt.Sprintf("%s | %s | %s | %s | %s | %s | %s | %s | %s\n\n", + tr.MenuAdd, tr.MenuEdit, tr.MenuExport, tr.MenuDelete, tr.MenuSearch, tr.MenuBrowse, tr.MenuFilter, tr.MenuToggleComplete, tr.MenuLang)) b.WriteString(tr.ListTitle + filterInfo + categoryInfo + "\n") if m.ready { @@ -911,8 +967,10 @@ func (m *model) handleAddTodo() { Tags: tags, CreatedAt: now, ModifiedAt: now, + Completed: false, }) saveTodos(m.todos) + m.sortTodos() m.applyFilter() m.statusMsg = tr.StatusSaved m.state = StateViewTodos @@ -940,6 +998,7 @@ func (m *model) handleEditTodo() { } } saveTodos(m.todos) + m.sortTodos() m.applyFilter() m.statusMsg = tr.StatusEdited m.state = StateViewTodos @@ -958,9 +1017,46 @@ func (m *model) handleDeleteTodo() { m.todos = append(m.todos[:todoIndex], m.todos[todoIndex+1:]...) saveTodos(m.todos) } + m.sortTodos() m.applyFilter() } +func (m *model) toggleTodoCompleted() { + tr := Translations[m.lang] + selectedID := m.filteredTodos[m.cursor].ID + + for i, t := range m.todos { + if t.ID == selectedID { + m.todos[i].Completed = !m.todos[i].Completed + m.todos[i].ModifiedAt = time.Now() + break + } + } + saveTodos(m.todos) + m.sortTodos() + m.applyFilter() + + // Find the toggled todo in filtered list to update cursor + for i, t := range m.filteredTodos { + if t.ID == selectedID { + m.cursor = i + break + } + } + + // Find the todo to check its completed status for status message + for _, t := range m.todos { + if t.ID == selectedID { + if t.Completed { + m.statusMsg = tr.StatusCompleted + } else { + m.statusMsg = tr.StatusUncompleted + } + break + } + } +} + func exportTodo(todos []Todo, id int64) { for _, todo := range todos { if todo.ID == id {