feat: add DONE state option and done marker to settings

This commit is contained in:
Taybin Rutkin 2026-07-21 17:53:04 -04:00
parent 517b60ecd9
commit 5d8e64f8a1
No known key found for this signature in database
GPG key ID: 605FA2C570B6AC9E
4 changed files with 74 additions and 4 deletions

View file

@ -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 {

View file

@ -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")
}
}
}

View file

@ -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 {

View file

@ -107,7 +107,12 @@ func (m *uiModel) updateSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
case settingsSectionTags:
m.addNewTag()
case settingsSectionStates:
// 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
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