package main import ( "fmt" "os" "strings" "time" "github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) var ( appStyle = lipgloss.NewStyle(). Padding(1, 2). Border(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("62")) titleStyle = lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color("#FAFAFA")). Background(lipgloss.Color("#7D56F4")). Padding(0, 1). MarginBottom(1) selectedStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#7D56F4")). Bold(true) statusStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("205")) focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) cursorStyle = focusedStyle.Copy() confirmBoxStyle = lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("205")). Padding(1, 2).Align(lipgloss.Center) priorityStyleLow = lipgloss.NewStyle(). Foreground(lipgloss.Color("#2ECC71")) // Zelená priorityStyleMedium = lipgloss.NewStyle(). Foreground(lipgloss.Color("#F39C12")) // Oranžová priorityStyleHigh = lipgloss.NewStyle(). Foreground(lipgloss.Color("#E74C3C")) // Červená categoryPersonal = lipgloss.NewStyle(). Foreground(lipgloss.Color("#3498DB")) // Modrá categoryWork = lipgloss.NewStyle(). Foreground(lipgloss.Color("#9B59B6")) // Fialová completedStyle = lipgloss.NewStyle(). Strikethrough(true). Foreground(lipgloss.Color("#95A5A6")) // Šedá pro splněné úkoly ) // Priority levels const ( PriorityLow = iota PriorityMedium PriorityHigh ) // Category types const ( CategoryPersonal = iota CategoryWork ) // Priority to string mapping var priorityLabels = map[int]string{ PriorityLow: "Nízká", PriorityMedium: "Střední", PriorityHigh: "Vysoká", } var priorityLabelsEn = map[int]string{ PriorityLow: "Low", PriorityMedium: "Medium", PriorityHigh: "High", } // Category to string mapping var categoryLabels = map[int]string{ CategoryPersonal: "Osobní", CategoryWork: "Pracovní", } var categoryLabelsEn = map[int]string{ CategoryPersonal: "Personal", CategoryWork: "Work", } type AppState int const ( StateStartup AppState = iota StateViewTodos StateViewTodoModal StateAddTodo StateEditTodo StateSearch StateBrowseTags StateConfirmDelete StateFilterCategory ) type Todo struct { ID int64 `json:"id"` Title string `json:"title"` Description string `json:"description"` Priority int `json:"priority"` Category int `json:"category"` Tags []string `json:"tags"` CreatedAt time.Time `json:"created_at"` ModifiedAt time.Time `json:"modified_at"` Completed bool `json:"completed"` } type model struct { todos []Todo filteredTodos []Todo cursor int state AppState browseTags []string filterText string textInputs []textinput.Model viewport viewport.Model ready bool focusIndex int selectedTodoID int64 statusMsg string lang string width int height int currentCategoryFilter int // -1 = no filter, 0 = personal, 1 = work newTodoPriority int // For tracking priority when adding new todo newTodoCategory int // For tracking category when adding new todo } func NewModel() model { inputs := make([]textinput.Model, 3) // title, description, tags for i := range inputs { t := textinput.New() t.Cursor.Style = cursorStyle inputs[i] = t } inputs[0].Focus() inputs[0].PromptStyle = focusedStyle inputs[0].TextStyle = focusedStyle m := model{ todos: loadTodos(), cursor: 0, state: StateStartup, textInputs: inputs, focusIndex: 0, lang: "cs", currentCategoryFilter: -1, newTodoPriority: PriorityMedium, newTodoCategory: CategoryPersonal, } m.sortTodos() m.applyFilter() m.updatePlaceholders() return m } 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 } else { m.filteredTodos = []Todo{} query := strings.ToLower(m.filterText) for _, t := range m.todos { // Apply text filter textMatch := true if m.filterText != "" { textMatch = strings.Contains(strings.ToLower(t.Title), query) || strings.Contains(strings.ToLower(t.Description), query) || strings.Contains(strings.ToLower(strings.Join(t.Tags, " ")), query) } // Apply category filter categoryMatch := true if m.currentCategoryFilter != -1 { categoryMatch = t.Category == m.currentCategoryFilter } if textMatch && categoryMatch { m.filteredTodos = append(m.filteredTodos, t) } } } // 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 { m.cursor = len(m.filteredTodos) - 1 } } } func (m *model) getBrowseTags() { set := make(map[string]bool) for _, t := range m.todos { for _, tag := range t.Tags { if tag != "" { set[tag] = true } } } m.browseTags = []string{} for k := range set { m.browseTags = append(m.browseTags, k) } // Manual sort for i := 0; i < len(m.browseTags); i++ { for j := i + 1; j < len(m.browseTags); j++ { if m.browseTags[i] > m.browseTags[j] { m.browseTags[i], m.browseTags[j] = m.browseTags[j], m.browseTags[i] } } } m.cursor = 0 } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd tr := Translations[m.lang] switch msg := msg.(type) { case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height headerHeight := 8 footerHeight := 3 vpHeight := msg.Height - headerHeight - footerHeight if vpHeight < 1 { vpHeight = 1 } if !m.ready { m.viewport = viewport.New(msg.Width-6, vpHeight) m.ready = true } else { m.viewport.Width = msg.Width - 6 m.viewport.Height = vpHeight } case tea.KeyMsg: switch msg.String() { case "ctrl+c": return m, tea.Quit case "esc": if m.state == StateConfirmDelete { m.state = StateViewTodos m.statusMsg = "" return m, nil } if m.state == StateViewTodos { if m.filterText != "" { m.filterText = "" m.applyFilter() return m, nil } return m, tea.Quit } if m.state == StateFilterCategory { m.state = StateViewTodos m.currentCategoryFilter = -1 m.applyFilter() return m, nil } m.resetToView() return m, nil case "q": if m.state == StateStartup { return m, tea.Quit } if m.state == StateConfirmDelete { m.state = StateViewTodos m.statusMsg = "" return m, nil } if m.state == StateViewTodoModal { m.state = StateViewTodos headerHeight := 8 footerHeight := 3 vpHeight := m.height - headerHeight - footerHeight if vpHeight < 1 { vpHeight = 1 } m.viewport.Width = m.width - 6 m.viewport.Height = vpHeight return m, nil } if m.state == StateViewTodos { if m.filterText != "" { m.filterText = "" m.applyFilter() return m, nil } return m, tea.Quit } m.resetToView() return m, nil case "l": if m.state == StateViewTodos { if m.lang == "cs" { m.lang = "en" } else { m.lang = "cs" } m.updatePlaceholders() return m, nil } case "/": if m.state == StateViewTodos { m.state = StateSearch m.textInputs[2].Focus() m.textInputs[2].SetValue("") return m, nil } case "tab", "shift+tab": if m.state == StateAddTodo || m.state == StateEditTodo { if msg.String() == "shift+tab" { m.focusIndex-- } else { m.focusIndex++ } if m.focusIndex > 2 { m.focusIndex = 0 } if m.focusIndex < 0 { m.focusIndex = 2 } return m.updateFocus() } case "up", "k": if m.state == StateViewTodos && m.cursor > 0 { m.cursor-- m.viewport.LineUp(1) } if m.state == StateViewTodoModal { m.viewport.LineUp(1) } if m.state == StateBrowseTags && m.cursor > 0 { m.cursor-- } case "down", "j": if m.state == StateViewTodos { max := len(m.filteredTodos) if m.cursor < max-1 { m.cursor++ m.viewport.LineDown(1) } } if m.state == StateViewTodoModal { m.viewport.LineDown(1) } if m.state == StateBrowseTags { max := len(m.browseTags) if m.cursor < max-1 { m.cursor++ } } case "enter": switch m.state { case StateStartup: m.state = StateViewTodos case StateViewTodos: if len(m.filteredTodos) > 0 { m.state = StateViewTodoModal m.selectedTodoID = m.filteredTodos[m.cursor].ID var t Todo for _, todo := range m.todos { if todo.ID == m.selectedTodoID { t = todo break } } categoryLabel := categoryLabels[t.Category] if m.lang == "en" { categoryLabel = categoryLabelsEn[t.Category] } priorityLabel := priorityLabels[t.Priority] if m.lang == "en" { priorityLabel = priorityLabelsEn[t.Priority] } modalContent := fmt.Sprintf("%s\n---\n[%d] %s\n\n%s\n\nKategorie: %s\nPriorita: %s\n\nTagy: %s\n\nVytvořeno: %s", tr.HelpModal, t.ID, t.Title, t.Description, categoryLabel, priorityLabel, strings.Join(t.Tags, ", "), t.CreatedAt.Format("2006-01-02 15:04")) modalWidth := m.width - 20 if modalWidth < 40 { modalWidth = 40 } modalHeight := m.height - 10 if modalHeight < 10 { modalHeight = 10 } m.viewport.Width = modalWidth m.viewport.Height = modalHeight m.viewport.SetContent(modalContent) m.viewport.YOffset = 0 } case StateSearch: m.filterText = m.textInputs[2].Value() m.applyFilter() m.state = StateViewTodos case StateBrowseTags: if len(m.browseTags) > 0 { m.filterText = m.browseTags[m.cursor] m.applyFilter() } m.state = StateViewTodos case StateAddTodo: m.handleAddTodo() m.state = StateViewTodos case StateEditTodo: m.handleEditTodo() m.state = StateViewTodos case StateConfirmDelete: if m.focusIndex == 0 { m.handleDeleteTodo() m.state = StateViewTodos m.statusMsg = tr.StatusDeleted return m, nil } else { m.state = StateViewTodos m.statusMsg = "" return m, nil } } case "c": if m.state == StateViewTodos { m.state = StateFilterCategory return m, nil } case "b": if m.state == StateViewTodos { m.state = StateBrowseTags m.getBrowseTags() return m, nil } case "1": if m.state == StateViewTodos { m.state = StateAddTodo m.resetInputs() m.textInputs[0].SetValue("") m.updatePlaceholders() // Reset priority and category for new todo m.newTodoPriority = PriorityMedium m.newTodoCategory = CategoryPersonal return m, nil } case "2": if m.state == StateViewTodos && len(m.filteredTodos) > 0 { m.state = StateEditTodo m.selectedTodoID = m.filteredTodos[m.cursor].ID m.populateInputs(m.filteredTodos[m.cursor]) return m, nil } if m.state == StateViewTodoModal { for _, t := range m.todos { if t.ID == m.selectedTodoID { m.state = StateEditTodo m.populateInputs(t) break } } return m, nil } case "3": if m.state == StateViewTodos && len(m.filteredTodos) > 0 { exportTodo(m.todos, m.filteredTodos[m.cursor].ID) m.statusMsg = tr.StatusExported } case "4": if m.state == StateViewTodos && len(m.filteredTodos) > 0 { m.state = StateConfirmDelete 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 currentPriority := m.newTodoPriority if m.state == StateEditTodo { for _, t := range m.todos { if t.ID == m.selectedTodoID { currentPriority = t.Priority break } } } newPriority := (currentPriority + 1) % 3 if m.state == StateAddTodo { // For new todos, update the tracking variable m.newTodoPriority = newPriority priorityLabel := priorityLabels[newPriority] if m.lang == "en" { priorityLabel = priorityLabelsEn[newPriority] } m.statusMsg = fmt.Sprintf("%s: %s", tr.LabelPriority, priorityLabel) } else { // For existing todos, update directly for i, t := range m.todos { if t.ID == m.selectedTodoID { m.todos[i].Priority = newPriority m.todos[i].ModifiedAt = time.Now() break } } saveTodos(m.todos) priorityLabel := priorityLabels[newPriority] if m.lang == "en" { priorityLabel = priorityLabelsEn[newPriority] } m.statusMsg = fmt.Sprintf("%s: %s", tr.LabelPriority, priorityLabel) } return m, nil } case "-": if m.state == StateAddTodo || m.state == StateEditTodo { // Cycle category currentCategory := m.newTodoCategory if m.state == StateEditTodo { for _, t := range m.todos { if t.ID == m.selectedTodoID { currentCategory = t.Category break } } } newCategory := (currentCategory + 1) % 2 if m.state == StateAddTodo { // For new todos, update the tracking variable m.newTodoCategory = newCategory categoryLabel := categoryLabels[newCategory] if m.lang == "en" { categoryLabel = categoryLabelsEn[newCategory] } m.statusMsg = fmt.Sprintf("%s: %s", tr.LabelCategory, categoryLabel) } else { // For existing todos, update directly for i, t := range m.todos { if t.ID == m.selectedTodoID { m.todos[i].Category = newCategory m.todos[i].ModifiedAt = time.Now() break } } saveTodos(m.todos) categoryLabel := categoryLabels[newCategory] if m.lang == "en" { categoryLabel = categoryLabelsEn[newCategory] } m.statusMsg = fmt.Sprintf("%s: %s", tr.LabelCategory, categoryLabel) } return m, nil } } if m.state == StateSearch { m.textInputs[2], cmd = m.textInputs[2].Update(msg) return m, cmd } if m.state == StateAddTodo || m.state == StateEditTodo { idx := m.focusIndex m.textInputs[idx], cmd = m.textInputs[idx].Update(msg) return m, cmd } if m.state == StateConfirmDelete { switch msg.String() { case "y": m.handleDeleteTodo() m.state = StateViewTodos m.statusMsg = tr.StatusDeleted return m, nil case "n", "esc": m.state = StateViewTodos m.statusMsg = "" return m, nil } } if m.state == StateFilterCategory { switch msg.String() { case "1": m.currentCategoryFilter = CategoryPersonal m.applyFilter() m.state = StateViewTodos m.statusMsg = fmt.Sprintf("Filtr: %s", categoryLabels[CategoryPersonal]) case "2": m.currentCategoryFilter = CategoryWork m.applyFilter() m.state = StateViewTodos m.statusMsg = fmt.Sprintf("Filtr: %s", categoryLabels[CategoryWork]) case "0", "esc": m.currentCategoryFilter = -1 m.applyFilter() m.state = StateViewTodos m.statusMsg = "Filtr zrušen" } } } return m, nil } func (m *model) updateFocus() (tea.Model, tea.Cmd) { var cmds []tea.Cmd for i := range m.textInputs { m.textInputs[i].Blur() m.textInputs[i].PromptStyle = lipgloss.NewStyle() } switch m.focusIndex { case 0: cmds = append(cmds, m.textInputs[0].Focus()) m.textInputs[0].PromptStyle = focusedStyle case 1: cmds = append(cmds, m.textInputs[1].Focus()) m.textInputs[1].PromptStyle = focusedStyle case 2: cmds = append(cmds, m.textInputs[2].Focus()) m.textInputs[2].PromptStyle = focusedStyle } return m, tea.Batch(cmds...) } func (m model) View() string { tr := Translations[m.lang] var b strings.Builder b.WriteString(titleStyle.Render(tr.Title)) b.WriteString("\n") if m.statusMsg != "" { b.WriteString(statusStyle.Render(m.statusMsg) + "\n\n") } switch m.state { case StateStartup: eagle := ` _ ( ` + lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Render("`") + ` \ ` + lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Render("\\") + ` / / _ __ / / ( \ ( "---" / / / \ / / / \ /-----' / \ / / \ / / '-------' ` b.WriteString(lipgloss.Place(m.width-12, 14, lipgloss.Center, lipgloss.Center, eagle + "\n" + lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#7D56F4")).Render("DOXA-TODO 🦅") + "\n\n" + "Local-First Todo Manager\n\n" + "Enter: Start | q: Quit\n\n" + "License: MIT | © 2026 Kankys")) case StateSearch: b.WriteString(tr.SearchTitle + "\n" + m.textInputs[2].View() + "\n\n" + tr.HelpForm) case StateBrowseTags: b.WriteString(fmt.Sprintf("--- %s ---\n", tr.BrowseTags)) b.WriteString("Enter: Filter | q: Back\n\n") for i, item := range m.browseTags { cursor := " " if m.cursor == i { cursor = "> " } b.WriteString(fmt.Sprintf("%s%s\n", cursor, item)) } case StateViewTodos: filterInfo := "" if m.filterText != "" { filterInfo = fmt.Sprintf(" [Text: %s]", m.filterText) } categoryInfo := "" if m.currentCategoryFilter != -1 { if m.lang == "cs" { categoryInfo = fmt.Sprintf(" [Kategorie: %s]", categoryLabels[m.currentCategoryFilter]) } else { categoryInfo = fmt.Sprintf(" [Category: %s]", categoryLabelsEn[m.currentCategoryFilter]) } } var listBuilder strings.Builder if len(m.filteredTodos) == 0 { listBuilder.WriteString(tr.NoTodos + "\n") } else { for i, t := range m.filteredTodos { priorityIcon := "" switch t.Priority { case PriorityLow: priorityIcon = priorityStyleLow.Render("●") case PriorityMedium: priorityIcon = priorityStyleMedium.Render("●") case PriorityHigh: priorityIcon = priorityStyleHigh.Render("●") } categoryIcon := "" switch t.Category { case CategoryPersonal: categoryIcon = categoryPersonal.Render("🏠") case CategoryWork: categoryIcon = categoryWork.Render("💼") } tagStr := "" if len(t.Tags) > 0 { tagStr = " [" + strings.Join(t.Tags, ", ") + "]" } cursor := " " if m.cursor == i { cursor = selectedStyle.Render("> ") } 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)) } } if m.ready { m.viewport.SetContent(listBuilder.String()) } 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 { b.WriteString(m.viewport.View() + "\n") } else { b.WriteString(listBuilder.String()) } b.WriteString("\n" + tr.HelpList) case StateAddTodo, StateEditTodo: title := tr.AddTitle if m.state == StateEditTodo { title = fmt.Sprintf("%s (%d)", tr.EditTitle, m.selectedTodoID) } b.WriteString(title + "\n\n") b.WriteString(fmt.Sprintf("%-8s %s\n", tr.LabelTitle, m.textInputs[0].View())) b.WriteString(fmt.Sprintf("%-8s %s\n", tr.LabelDescription, m.textInputs[1].View())) b.WriteString(fmt.Sprintf("%-8s %s\n", tr.LabelTags, m.textInputs[2].View())) // Show priority selection currentPriority := m.newTodoPriority if m.state == StateEditTodo { for _, t := range m.todos { if t.ID == m.selectedTodoID { currentPriority = t.Priority break } } } priorityLabel := priorityLabels[currentPriority] if m.lang == "en" { priorityLabel = priorityLabelsEn[currentPriority] } b.WriteString(fmt.Sprintf("\n%s: %s (+: Zvýšit prioritu)\n", tr.LabelPriority, priorityStyle(currentPriority).Render(priorityLabel))) // Show category selection currentCategory := m.newTodoCategory if m.state == StateEditTodo { for _, t := range m.todos { if t.ID == m.selectedTodoID { currentCategory = t.Category break } } } categoryLabel := categoryLabels[currentCategory] if m.lang == "en" { categoryLabel = categoryLabelsEn[currentCategory] } b.WriteString(fmt.Sprintf("%s: %s (-: Změnit kategorii)\n", tr.LabelCategory, categoryStyle(currentCategory).Render(categoryLabel))) b.WriteString("\n" + tr.HelpForm) case StateViewTodoModal: if m.ready { b.WriteString(confirmBoxStyle.Render(m.viewport.View())) } else { b.WriteString(confirmBoxStyle.Render("Načítání...")) } case StateConfirmDelete: var todoToDelete Todo found := false for _, t := range m.todos { if t.ID == m.selectedTodoID { todoToDelete = t found = true break } } if !found { m.state = StateViewTodos m.statusMsg = "Chyba: Úkol k smazání nebyl nalezen!" return "" } confirmText := fmt.Sprintf("%s\n\n[%d] %s\nPopis: %s\n%s | %s", tr.ConfirmDelete, todoToDelete.ID, todoToDelete.Title, truncate(todoToDelete.Description, 40), tr.ConfirmYes, tr.ConfirmNo, ) b.WriteString(confirmBoxStyle.Render(confirmText)) case StateFilterCategory: b.WriteString("--- " + tr.FilterCategoryTitle + " ---\n\n") b.WriteString("1: " + categoryLabels[CategoryPersonal] + "\n") b.WriteString("2: " + categoryLabels[CategoryWork] + "\n") b.WriteString("0: " + tr.FilterCategoryNone + "\n\n") b.WriteString(tr.FilterCategoryHelp) } content := b.String() if m.width > 0 && m.height > 0 { return appStyle.Width(m.width - 6).Height(m.height - 4).Render(content) } return appStyle.Render(content) } func (m *model) updatePlaceholders() { tr := Translations[m.lang] m.textInputs[0].Placeholder = tr.PlaceholderTitle m.textInputs[1].Placeholder = tr.PlaceholderDescription m.textInputs[2].Placeholder = tr.PlaceholderTags } func (m *model) resetToView() { m.state = StateViewTodos m.statusMsg = "" m.filterText = "" m.applyFilter() m.resetInputs() } func (m *model) resetInputs() { for i := range m.textInputs { m.textInputs[i].SetValue("") m.textInputs[i].Blur() } m.focusIndex = 0 m.updateFocus() } func (m *model) populateInputs(t Todo) { m.textInputs[0].SetValue(t.Title) m.textInputs[1].SetValue(t.Description) m.textInputs[2].SetValue(strings.Join(t.Tags, ", ")) m.focusIndex = 0 m.updateFocus() } func (m *model) handleAddTodo() { tr := Translations[m.lang] title := m.textInputs[0].Value() description := m.textInputs[1].Value() if title == "" { m.statusMsg = tr.ErrRequired return } tags := []string{} if m.textInputs[2].Value() != "" { tags = strings.Split(m.textInputs[2].Value(), ",") for i := range tags { tags[i] = strings.TrimSpace(tags[i]) } } // Use the tracked priority and category priority := m.newTodoPriority category := m.newTodoCategory now := time.Now() m.todos = append(m.todos, Todo{ ID: now.UnixMilli(), Title: title, Description: description, Priority: priority, Category: category, Tags: tags, CreatedAt: now, ModifiedAt: now, Completed: false, }) saveTodos(m.todos) m.sortTodos() m.applyFilter() m.statusMsg = tr.StatusSaved m.state = StateViewTodos // Reset priority and category for next todo m.newTodoPriority = PriorityMedium m.newTodoCategory = CategoryPersonal } func (m *model) handleEditTodo() { tr := Translations[m.lang] for i, t := range m.todos { if t.ID == m.selectedTodoID { m.todos[i].Title = m.textInputs[0].Value() m.todos[i].Description = m.textInputs[1].Value() tags := []string{} if m.textInputs[2].Value() != "" { tags = strings.Split(m.textInputs[2].Value(), ",") for j := range tags { tags[j] = strings.TrimSpace(tags[j]) } } m.todos[i].Tags = tags m.todos[i].ModifiedAt = time.Now() break } } saveTodos(m.todos) m.sortTodos() m.applyFilter() m.statusMsg = tr.StatusEdited m.state = StateViewTodos } func (m *model) handleDeleteTodo() { todoIndex := -1 for i, t := range m.todos { if t.ID == m.selectedTodoID { todoIndex = i break } } if todoIndex != -1 { 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 { homeDir, err := os.UserHomeDir() if err != nil { fmt.Printf("Chyba při zjišťování domovského adresáře: %v\n", err) return } exportDir := fmt.Sprintf("%s/doxa-todo-export", homeDir) if err := os.MkdirAll(exportDir, 0755); err != nil { fmt.Printf("Chyba při vytváření adresáře pro export: %v\n", err) return } categoryLabel := "Osobní" if todo.Category == CategoryWork { categoryLabel = "Pracovní" } priorityLabel := priorityLabels[todo.Priority] fileName := fmt.Sprintf("%s_%d.txt", strings.ReplaceAll(todo.Title, " ", "_"), todo.ID) fullPath := fmt.Sprintf("%s/%s", exportDir, fileName) file, err := os.Create(fullPath) if err != nil { fmt.Printf("Chyba při vytváření souboru pro export: %v\n", err) return } defer file.Close() content := fmt.Sprintf("Název: %s\nKategorie: %s\nPriorita: %s\nTagy: %s\nVytvořeno: %s\n\n%s", todo.Title, categoryLabel, priorityLabel, strings.Join(todo.Tags, ", "), todo.CreatedAt.Format("2006-01-02 15:04"), todo.Description) _, _ = file.WriteString(content) return } } } func truncate(s string, max int) string { if len(s) > max { return strings.TrimSpace(s[:max]) + "..." } return s } func priorityStyle(priority int) lipgloss.Style { switch priority { case PriorityLow: return priorityStyleLow case PriorityMedium: return priorityStyleMedium case PriorityHigh: return priorityStyleHigh } return lipgloss.NewStyle() } func categoryStyle(category int) lipgloss.Style { switch category { case CategoryPersonal: return categoryPersonal case CategoryWork: return categoryWork } return lipgloss.NewStyle() } type errMsg error