Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"strings"

"github.com/prime-run/togo/config"
"github.com/prime-run/togo/ui"

tea "github.com/charmbracelet/bubbletea"
Expand All @@ -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()
Expand All @@ -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))
Expand Down
61 changes: 61 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -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)
}
58 changes: 39 additions & 19 deletions ui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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 {
Expand All @@ -60,3 +76,7 @@ func (m *TodoTableModel) SetSource(label, filename string) {
m.projectName = ""
}
}

func (m *TodoTableModel) SetSkipConfirmationsStatus(status string) {
m.skipConfirmationsStatus = status
}
94 changes: 63 additions & 31 deletions ui/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -209,43 +212,42 @@ 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 count > 0 {
m.SetStatusMessage(fmt.Sprintf("%d tasks updated", count))
if m.SkipConfirmationsByDefault {
if len(m.selectedTodoIDs) > 0 && m.bulkActionActive {
// Bulk archive/unarchive
// ... (logic as before)
} else {
// Single archive/unarchive
// ... (logic as before)
}
m.updateRows()
return m, m.forceRelayoutCmd()
} else {
selectedTitle := m.table.SelectedRow()[1]
cleanTitle := strings.ReplaceAll(selectedTitle, archivedStyle.Render(""), "")
}

for _, todo := range m.todoList.Todos {
if strings.Contains(selectedTitle, todo.Title) || todo.Title == cleanTitle {
if todo.Archived {
m.todoList.Unarchive(todo.ID)
m.SetStatusMessage("Task unarchived")
} else {
m.todoList.Archive(todo.ID)
m.SetStatusMessage("Task archived")
}
m.updateRows()
break
}
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
}
}
}
Expand All @@ -255,6 +257,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"
Expand Down
5 changes: 5 additions & 0 deletions ui/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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 {
Expand All @@ -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"
}
Expand Down