diff --git a/cmd/root.go b/cmd/root.go index 19efe91..01d0fbb 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/prime-run/togo/config" "github.com/prime-run/togo/ui" tea "github.com/charmbracelet/bubbletea" @@ -12,17 +13,35 @@ import ( var TodoFileName = "todos.json" var sourceFlag string = "project" +var skipConfirmations bool var rootCmd = &cobra.Command{ Use: "togo", Short: "A simple todo application", Long: `A simple todo application that lets you manage your tasks from the terminal.`, Run: func(cmd *cobra.Command, args []string) { + cfg, err := config.Load() + if err != nil { + handleErrorAndExit(err, "Error loading config:") + } + + // Flag overrides config + if !skipConfirmations { + skipConfirmations = cfg.SkipConfirmations + } + todoList := loadTodoListOrExit() tableModel := ui.NewTodoTable(todoList) tableModel.SetSource(sourceFlag, TodoFileName) - _, err := tea.NewProgram(tableModel, tea.WithAltScreen()).Run() + tableModel.SetConfig(cfg) + tableModel.SkipConfirmationsByDefault = skipConfirmations + if skipConfirmations { + tableModel.SetSkipConfirmationsStatus("on") + } else { + tableModel.SetSkipConfirmationsStatus("off") + } + _, err = tea.NewProgram(tableModel, tea.WithAltScreen()).Run() handleErrorAndExit(err, "Error running program:") finalSource := tableModel.GetSourceLabel() @@ -38,8 +57,8 @@ func Execute() error { } func init() { - rootCmd.PersistentFlags().StringVarP(&sourceFlag, "source", "s", "project", "todo source: project or global") + rootCmd.PersistentFlags().BoolVarP(&skipConfirmations, "skip-confirmations", "y", false, "skip confirmations for delete/archive") rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { s := strings.ToLower(strings.TrimSpace(sourceFlag)) diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..78d3c62 --- /dev/null +++ b/config/config.go @@ -0,0 +1,61 @@ +package config + +import ( + "encoding/json" + "os" + "path/filepath" +) + +type Config struct { + SkipConfirmations bool `json:"skip_confirmations"` +} + +func Load() (*Config, error) { + home, err := os.UserHomeDir() + if err != nil { + return nil, err + } + + configDir := filepath.Join(home, ".togo") + if err := os.MkdirAll(configDir, 0755); err != nil { + return nil, err + } + + configFile := filepath.Join(configDir, "config.json") + if _, err := os.Stat(configFile); os.IsNotExist(err) { + cfg := &Config{SkipConfirmations: false} + if err := Save(cfg); err != nil { + return nil, err + } + return cfg, nil + } + + data, err := os.ReadFile(configFile) + if err != nil { + return nil, err + } + + var cfg Config + if err := json.Unmarshal(data, &cfg); err != nil { + return nil, err + } + + return &cfg, nil +} + +func Save(cfg *Config) error { + home, err := os.UserHomeDir() + if err != nil { + return err + } + + configDir := filepath.Join(home, ".togo") + configFile := filepath.Join(configDir, "config.json") + + data, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return err + } + + return os.WriteFile(configFile, data, 0644) +} diff --git a/ui/model.go b/ui/model.go index 743187b..3ffc4cc 100644 --- a/ui/model.go +++ b/ui/model.go @@ -3,6 +3,7 @@ package ui import ( "github.com/charmbracelet/bubbles/table" "github.com/charmbracelet/bubbles/textinput" + "github.com/prime-run/togo/config" "github.com/prime-run/togo/model" ) @@ -17,25 +18,40 @@ const ( ) type TodoTableModel struct { - todoList *model.TodoList - table table.Model - mode Mode - confirmAction string - actionTitle string - viewTaskID int - width int - height int - selectedTodoIDs map[int]bool - bulkActionActive bool - textInput textinput.Model - showArchived bool - showAll bool - showArchivedOnly bool - statusMessage string - showHelp bool - sourceLabel string - todoFileName string - projectName string + todoList *model.TodoList + table table.Model + mode Mode + confirmAction string + actionTitle string + viewTaskID int + width int + height int + selectedTodoIDs map[int]bool + bulkActionActive bool + textInput textinput.Model + showArchived bool + showAll bool + showArchivedOnly bool + statusMessage string + showHelp bool + sourceLabel string + todoFileName string + projectName string + SkipConfirmationsByDefault bool + skipConfirmationsStatus string + config *config.Config +} + +func (m *TodoTableModel) SetConfig(cfg *config.Config) { + m.config = cfg +} + +func (m *TodoTableModel) SaveConfig() error { + if m.config != nil { + m.config.SkipConfirmations = m.SkipConfirmationsByDefault + return config.Save(m.config) + } + return nil } func (m TodoTableModel) GetSourceLabel() string { @@ -60,3 +76,7 @@ func (m *TodoTableModel) SetSource(label, filename string) { m.projectName = "" } } + +func (m *TodoTableModel) SetSkipConfirmationsStatus(status string) { + m.skipConfirmationsStatus = status +} diff --git a/ui/update.go b/ui/update.go index 4176b25..516b23c 100644 --- a/ui/update.go +++ b/ui/update.go @@ -65,6 +65,9 @@ func (m TodoTableModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } } + if err := m.todoList.SaveWithSource(m.todoFileName, m.sourceLabel); err != nil { + m.SetStatusMessage("save failed: " + err.Error()) + } } } else if m.mode == ModeArchiveConfirm { if len(m.selectedTodoIDs) > 0 && m.bulkActionActive { @@ -209,33 +212,63 @@ func (m TodoTableModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.updateRows() return m, m.forceRelayoutCmd() } + case "c": + m.SkipConfirmationsByDefault = !m.SkipConfirmationsByDefault + if m.SkipConfirmationsByDefault { + m.skipConfirmationsStatus = "on" + m.SetStatusMessage("Confirmations are now off") + } else { + m.skipConfirmationsStatus = "off" + m.SetStatusMessage("Confirmations are now on") + } + if err := m.SaveConfig(); err != nil { + m.SetStatusMessage("Error saving config: " + err.Error()) + } + return m, nil case "n": if len(m.table.Rows()) > 0 { - if len(m.selectedTodoIDs) > 0 && m.bulkActionActive { - count := 0 - for id := range m.selectedTodoIDs { - todo := m.findTodoByID(id) - if todo != nil { - if todo.Archived { - m.todoList.Unarchive(id) - count++ - } else { - m.todoList.Archive(id) - count++ + if m.SkipConfirmationsByDefault { + if len(m.selectedTodoIDs) > 0 && m.bulkActionActive { + archivedCount := 0 + unarchivedCount := 0 + for id := range m.selectedTodoIDs { + todo := m.findTodoByID(id) + if todo != nil { + if todo.Archived { + m.todoList.Unarchive(id) + unarchivedCount++ + } else { + m.todoList.Archive(id) + archivedCount++ + } } } - } - if count > 0 { - m.SetStatusMessage(fmt.Sprintf("%d tasks updated", count)) - } - m.updateRows() - return m, m.forceRelayoutCmd() - } else { - selectedTitle := m.table.SelectedRow()[1] - cleanTitle := strings.ReplaceAll(selectedTitle, archivedStyle.Render(""), "") + m.selectedTodoIDs = make(map[int]bool) + m.bulkActionActive = false - for _, todo := range m.todoList.Todos { - if strings.Contains(selectedTitle, todo.Title) || todo.Title == cleanTitle { + status := []string{} + if archivedCount > 0 { + status = append(status, fmt.Sprintf("%d archived", archivedCount)) + } + if unarchivedCount > 0 { + status = append(status, fmt.Sprintf("%d unarchived", unarchivedCount)) + } + if len(status) > 0 { + m.SetStatusMessage(fmt.Sprintf("Tasks: %s", strings.Join(status, ", "))) + } + } else { + selectedIndex := m.table.Cursor() + var filteredTodos []model.Todo + if m.showAll { + filteredTodos = m.todoList.Todos + } else if m.showArchivedOnly { + filteredTodos = m.todoList.GetArchivedTodos() + } else { + filteredTodos = m.todoList.GetActiveTodos() + } + + if selectedIndex < len(filteredTodos) { + todo := filteredTodos[selectedIndex] if todo.Archived { m.todoList.Unarchive(todo.ID) m.SetStatusMessage("Task unarchived") @@ -243,10 +276,22 @@ func (m TodoTableModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.todoList.Archive(todo.ID) m.SetStatusMessage("Task archived") } - m.updateRows() - break } } + m.updateRows() + return m, m.forceRelayoutCmd() + } + + m.mode = ModeArchiveConfirm + if len(m.selectedTodoIDs) > 0 && m.bulkActionActive { + m.confirmAction = "archive" + } else { + selectedRow := m.table.SelectedRow() + if len(selectedRow) > 1 { + selectedTitle := selectedRow[1] + cleanTitle := strings.ReplaceAll(selectedTitle, archivedStyle.Render(""), "") + m.actionTitle = cleanTitle + } } } case "a": @@ -255,6 +300,36 @@ func (m TodoTableModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, textinput.Blink case "d": if len(m.table.Rows()) > 0 { + if m.SkipConfirmationsByDefault { + if len(m.selectedTodoIDs) > 0 && m.bulkActionActive { + count := len(m.selectedTodoIDs) + for id := range m.selectedTodoIDs { + m.todoList.Delete(id) + } + m.selectedTodoIDs = make(map[int]bool) + m.bulkActionActive = false + m.SetStatusMessage(fmt.Sprintf("%d tasks deleted", count)) + } else { + selectedRow := m.table.SelectedRow() + if len(selectedRow) > 1 { + selectedTitle := selectedRow[1] + cleanTitle := strings.ReplaceAll(selectedTitle, archivedStyle.Render(""), "") + for _, todo := range m.todoList.Todos { + if todo.Title == cleanTitle { + m.todoList.Delete(todo.ID) + m.SetStatusMessage("Task deleted") + break + } + } + } + } + if err := m.todoList.SaveWithSource(m.todoFileName, m.sourceLabel); err != nil { + m.SetStatusMessage("save failed: " + err.Error()) + } + m.updateRows() + return m, m.forceRelayoutCmd() + } + if len(m.selectedTodoIDs) > 0 && m.bulkActionActive { m.mode = ModeDeleteConfirm m.confirmAction = "delete" diff --git a/ui/view.go b/ui/view.go index 197cdbd..5276b18 100644 --- a/ui/view.go +++ b/ui/view.go @@ -77,6 +77,9 @@ func (m TodoTableModel) View() string { sourceText += " (" + m.projectName + ")" } } + if m.skipConfirmationsStatus != "" { + sourceText += " | skip confirmations: " + m.skipConfirmationsStatus + } leftSide := titleBarStyle.Render(listTitle + sourceText) rightSide := successMessageStyle.Render(m.statusMessage) @@ -100,6 +103,7 @@ func (m TodoTableModel) View() string { "\n→ " + confirmBtnStyle.Render("enter") + ": view details" + "\n→ " + confirmBtnStyle.Render("a") + ": add new task" + "\n→ " + confirmBtnStyle.Render("s") + ": switch source (project/global)" + + "\n→ " + confirmBtnStyle.Render("c") + ": toggle confirmations" + "\n→ " + confirmBtnStyle.Render("q") + ": quit" + "\n→ " + confirmBtnStyle.Render(".") + ": toggle help" } else { @@ -111,6 +115,7 @@ func (m TodoTableModel) View() string { "\n→ " + confirmBtnStyle.Render("enter") + ": view details" + "\n→ " + confirmBtnStyle.Render("a") + ": add new task" + "\n→ " + confirmBtnStyle.Render("s") + ": switch source (project/global)" + + "\n→ " + confirmBtnStyle.Render("c") + ": toggle confirmations" + "\n→ " + confirmBtnStyle.Render("q") + ": quit" + "\n→ " + confirmBtnStyle.Render(".") + ": toggle help" }