diff --git a/internal/config/config.go b/internal/config/config.go index b93c192..1dd585e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -498,6 +498,18 @@ func (c *Config) AddState(name, color string) { c.States.States = append(c.States.States, StateConfig{Name: name, Color: color}) } +// AddDoneState adds (or upgrades) a state and marks it as a done state. +func (c *Config) AddDoneState(name, color string) { + for i := range c.States.States { + if c.States.States[i].Name == name { + c.States.States[i].Color = color + c.States.States[i].Done = true + return + } + } + c.States.States = append(c.States.States, StateConfig{Name: name, Color: color, Done: true}) +} + // RemoveState removes a state from the configuration func (c *Config) RemoveState(name string) { for i, state := range c.States.States { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 3e22f80..d762436 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -49,3 +49,25 @@ func TestFillDefaultsLeavesExplicitDoneFlagsAlone(t *testing.T) { t.Error("last state (TODO) must NOT be auto-flagged when a done state already exists") } } + +func TestAddDoneState(t *testing.T) { + c := &Config{} + c.AddDoneState("CANCELLED", "240") + + if len(c.States.States) != 1 { + t.Fatalf("expected 1 state, got %d", len(c.States.States)) + } + s := c.States.States[0] + if s.Name != "CANCELLED" || s.Color != "240" || !s.Done { + t.Fatalf("unexpected state: %+v", s) + } + + // Adding an existing name upgrades it to done. + c.AddState("REVIEW", "99") + c.AddDoneState("REVIEW", "99") + for _, st := range c.States.States { + if st.Name == "REVIEW" && !st.Done { + t.Fatal("expected REVIEW upgraded to done") + } + } +} diff --git a/internal/ui/app.go b/internal/ui/app.go index 2de51e1..11e8a57 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -57,6 +57,7 @@ type uiModel struct { captureCursor int // Store cursor position when entering capture mode datepicker datepicker.Model dateTextFocused bool // within set-date modes: text field focused vs. calendar + addingDoneState bool // within add-state flow: create a done state } func InitialModel(orgFile *model.OrgFile, cfg *config.Config, captureMode bool, captureText string) uiModel { diff --git a/internal/ui/settings.go b/internal/ui/settings.go index 528b7d2..87898b1 100644 --- a/internal/ui/settings.go +++ b/internal/ui/settings.go @@ -107,7 +107,12 @@ func (m *uiModel) updateSettings(msg tea.Msg) (tea.Model, tea.Cmd) { case settingsSectionTags: m.addNewTag() case settingsSectionStates: - m.addNewState() + // Rows: [0]=default state, [1..N]=states, [N+1]=add state, [N+2]=add DONE state. + if m.settingsCursor == len(m.config.States.States)+2 { + m.addNewDoneState() + } else { + m.addNewState() + } case settingsSectionKeybindings: // Cannot add keybindings yet } @@ -136,7 +141,7 @@ func (m *uiModel) getSettingsItemCount() int { case settingsSectionTags: return len(m.config.Tags.Tags) + 1 // +1 for "Add new tag" option case settingsSectionStates: - return len(m.config.States.States) + 2 // +1 for "Default new task state" setting, +1 for "Add new state" option + return len(m.config.States.States) + 3 // default state + "Add new state" + "Add new DONE state" case settingsSectionKeybindings: return len(m.config.GetAllKeybindings()) default: @@ -474,6 +479,7 @@ func (m *uiModel) addNewTag() { // addNewState adds a new state func (m *uiModel) addNewState() { + m.addingDoneState = false m.textinput.SetValue("") m.textinput.Placeholder = "Enter state name" m.textinput.Focus() @@ -483,6 +489,16 @@ func (m *uiModel) addNewState() { m.mode = modeSettingsAddState } +// addNewDoneState adds a new state that is marked as done. +func (m *uiModel) addNewDoneState() { + m.addingDoneState = true + m.textinput.SetValue("") + m.textinput.Placeholder = "Enter DONE state name" + m.textinput.Focus() + m.textinput.Blur() + m.mode = modeSettingsAddState +} + // viewSettings renders the settings view func (m *uiModel) viewSettings() string { var content strings.Builder @@ -697,7 +713,7 @@ func (m *uiModel) viewSettingsStates() string { } endIdx := m.settingsScroll + availableHeight - totalItems := len(m.config.States.States) + 2 // +1 for default state, +1 for "Add new state" + totalItems := len(m.config.States.States) + 3 // +1 for default state, +1 for "Add new state", +1 for "Add new DONE state" // First show the default new task state setting (item 0) if 0 >= m.settingsScroll && 0 < endIdx { @@ -741,6 +757,9 @@ func (m *uiModel) viewSettingsStates() string { stateStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(state.Color)) line += stateStyle.Render(state.Name) line += fmt.Sprintf(" (color: %s)", state.Color) + if state.Done { + line += m.styles.statusStyle.Render(" ✓done") + } content.WriteString(line + "\n") } @@ -756,6 +775,17 @@ func (m *uiModel) viewSettingsStates() string { content.WriteString(m.styles.statusStyle.Render("+ Add new state (press 'c')") + "\n") } + // Add new DONE state option + addDoneIdx := len(m.config.States.States) + 2 + if addDoneIdx >= m.settingsScroll && addDoneIdx < endIdx { + if m.settingsCursor == addDoneIdx && !m.textinput.Focused() { + content.WriteString("▶ ") + } else { + content.WriteString(" ") + } + content.WriteString(m.styles.statusStyle.Render("+ Add new DONE state (press 'c')") + "\n") + } + // Add scroll indicator if needed if totalItems > availableHeight { scrollInfo := fmt.Sprintf("\n[Showing %d-%d of %d]", @@ -916,7 +946,11 @@ func (m *uiModel) updateSettingsAddState(msg tea.Msg) (tea.Model, tea.Cmd) { stateName := m.textinput.Value() if stateName != "" { // Default color - m.config.AddState(stateName, "99") + if m.addingDoneState { + m.config.AddDoneState(stateName, "99") + } else { + m.config.AddState(stateName, "99") + } // Auto-save if err := m.config.Save(); err != nil { m.setStatus(fmt.Sprintf("Error saving: %v", err)) @@ -924,6 +958,7 @@ func (m *uiModel) updateSettingsAddState(msg tea.Msg) (tea.Model, tea.Cmd) { m.setStatus(fmt.Sprintf("Added state '%s' (saved)", stateName)) } } + m.addingDoneState = false m.textinput.Blur() m.mode = modeSettings return m, nil