feat: add configurable done flag to TODO states

This commit is contained in:
Taybin Rutkin 2026-07-21 17:43:52 -04:00
parent 25dd231073
commit 5b10f4e229
No known key found for this signature in database
GPG key ID: 605FA2C570B6AC9E
2 changed files with 81 additions and 1 deletions

View file

@ -83,6 +83,7 @@ type TagsConfig struct {
type StateConfig struct {
Name string `toml:"name"`
Color string `toml:"color"`
Done bool `toml:"done,omitempty"`
}
// StatesConfig holds TODO state configurations
@ -163,7 +164,7 @@ func DefaultConfig() *Config {
{Name: "TODO", Color: "202"},
{Name: "PROG", Color: "220"},
{Name: "BLOCK", Color: "196"},
{Name: "DONE", Color: "34"},
{Name: "DONE", Color: "34", Done: true},
},
DefaultNewTaskState: "TODO",
},
@ -393,6 +394,21 @@ func (c *Config) fillDefaults() {
// Note: We don't fill DefaultNewTaskState if States.States is non-empty because
// an empty string is a valid intentional value meaning "no default state".
// Back-compat: if states exist but none is flagged done (config predates the
// `done` field), treat the last state as done to preserve prior behavior.
if len(c.States.States) > 0 {
anyDone := false
for _, s := range c.States.States {
if s.Done {
anyDone = true
break
}
}
if !anyDone {
c.States.States[len(c.States.States)-1].Done = true
}
}
// Fill UI if zero values
if c.UI.HelpTextWidth == 0 {
c.UI.HelpTextWidth = defaults.UI.HelpTextWidth
@ -511,6 +527,19 @@ func (c *Config) GetStateNames() []string {
return names
}
// IsDoneState reports whether the named state is configured as a done state.
func (c *Config) IsDoneState(name string) bool {
if name == "" {
return false
}
for _, state := range c.States.States {
if state.Name == name {
return state.Done
}
}
return false
}
// UpdateKeybinding updates a keybinding in the configuration
func (c *Config) UpdateKeybinding(action string, keys []string) error {
// Use reflection would be complex, so we handle specific cases

View file

@ -0,0 +1,51 @@
package config
import "testing"
func TestIsDoneState(t *testing.T) {
c := &Config{States: StatesConfig{States: []StateConfig{
{Name: "TODO", Color: "202"},
{Name: "DONE", Color: "34", Done: true},
{Name: "CANCELLED", Color: "240", Done: true},
}}}
cases := map[string]bool{"TODO": false, "DONE": true, "CANCELLED": true, "": false, "NOPE": false}
for name, want := range cases {
if got := c.IsDoneState(name); got != want {
t.Errorf("IsDoneState(%q) = %v, want %v", name, got, want)
}
}
}
func TestFillDefaultsMarksLastStateDoneWhenNoneFlagged(t *testing.T) {
c := &Config{States: StatesConfig{States: []StateConfig{
{Name: "TODO", Color: "202"},
{Name: "DONE", Color: "34"},
}}}
c.fillDefaults()
if c.States.States[len(c.States.States)-1].Name != "DONE" {
t.Fatalf("states reordered unexpectedly")
}
if !c.States.States[len(c.States.States)-1].Done {
t.Error("expected last state (DONE) to be flagged Done after migration")
}
if c.States.States[0].Done {
t.Error("expected first state (TODO) to remain not-done")
}
}
func TestFillDefaultsLeavesExplicitDoneFlagsAlone(t *testing.T) {
c := &Config{States: StatesConfig{States: []StateConfig{
{Name: "DONE", Color: "34", Done: true},
{Name: "TODO", Color: "202"},
}}}
c.fillDefaults()
if c.States.States[0].Name != "DONE" || !c.States.States[0].Done {
t.Error("explicit done flag on DONE should be preserved")
}
if c.States.States[1].Done {
t.Error("last state (TODO) must NOT be auto-flagged when a done state already exists")
}
}