package config import ( "fmt" "os" "path/filepath" "github.com/BurntSushi/toml" "github.com/charmbracelet/bubbles/key" ) // Config represents the application configuration type Config struct { Keybindings KeybindingsConfig `toml:"keybindings"` Colors ColorsConfig `toml:"colors"` Tags TagsConfig `toml:"tags"` States StatesConfig `toml:"states"` UI UIConfig `toml:"ui"` } // KeybindingsConfig holds all keybinding configurations type KeybindingsConfig struct { Up []string `toml:"up"` Down []string `toml:"down"` Left []string `toml:"left"` Right []string `toml:"right"` ShiftUp []string `toml:"shift_up"` ShiftDown []string `toml:"shift_down"` ShiftLeft []string `toml:"shift_left"` ShiftRight []string `toml:"shift_right"` Rename []string `toml:"rename"` CycleState []string `toml:"cycle_state"` ToggleFold []string `toml:"toggle_fold"` EditNotes []string `toml:"edit_notes"` ToggleView []string `toml:"toggle_view"` Capture []string `toml:"capture"` AddSubTask []string `toml:"add_subtask"` Delete []string `toml:"delete"` Save []string `toml:"save"` ToggleReorder []string `toml:"toggle_reorder"` ClockIn []string `toml:"clock_in"` ClockOut []string `toml:"clock_out"` SetDeadline []string `toml:"set_deadline"` SetPriority []string `toml:"set_priority"` SetEffort []string `toml:"set_effort"` Help []string `toml:"help"` Quit []string `toml:"quit"` Settings []string `toml:"settings"` TagItem []string `toml:"tag_item"` } // ColorsConfig holds color configurations type ColorsConfig struct { Todo string `toml:"todo"` Progress string `toml:"progress"` Blocked string `toml:"blocked"` Done string `toml:"done"` Cursor string `toml:"cursor"` Title string `toml:"title"` Scheduled string `toml:"scheduled"` Overdue string `toml:"overdue"` Status string `toml:"status"` Note string `toml:"note"` Folded string `toml:"folded"` } // TagConfig represents a single tag configuration type TagConfig struct { Name string `toml:"name"` Color string `toml:"color"` } // TagsConfig holds tag configurations type TagsConfig struct { Enabled bool `toml:"enabled"` Tags []TagConfig `toml:"tags"` DefaultTag string `toml:"default_tag"` } // StateConfig represents a single TODO state configuration type StateConfig struct { Name string `toml:"name"` Color string `toml:"color"` } // StatesConfig holds TODO state configurations type StatesConfig struct { States []StateConfig `toml:"states"` DefaultNewTaskState string `toml:"default_new_task_state"` } // UIConfig holds UI-related configurations type UIConfig struct { HelpTextWidth int `toml:"help_text_width"` MinTerminalWidth int `toml:"min_terminal_width"` AgendaDays int `toml:"agenda_days"` } // DefaultConfig returns the default configuration func DefaultConfig() *Config { return &Config{ Keybindings: KeybindingsConfig{ Up: []string{"up", "k"}, Down: []string{"down", "j"}, Left: []string{"left", "h"}, Right: []string{"right", "l"}, ShiftUp: []string{"shift+up"}, ShiftDown: []string{"shift+down"}, ShiftLeft: []string{"shift+left"}, ShiftRight: []string{"shift+right"}, Rename: []string{"R"}, CycleState: []string{"t", " "}, ToggleFold: []string{"tab"}, EditNotes: []string{"enter"}, ToggleView: []string{"a"}, Capture: []string{"c"}, AddSubTask: []string{"s"}, Delete: []string{"D"}, Save: []string{"ctrl+s"}, ToggleReorder: []string{"r"}, ClockIn: []string{"i"}, ClockOut: []string{"o"}, SetDeadline: []string{"d"}, SetPriority: []string{"p"}, SetEffort: []string{"e"}, Help: []string{"?"}, Quit: []string{"q", "ctrl+c"}, Settings: []string{","}, TagItem: []string{"#"}, }, Colors: ColorsConfig{ Todo: "202", Progress: "220", Blocked: "196", Done: "34", Cursor: "240", Title: "99", Scheduled: "141", Overdue: "196", Status: "241", Note: "246", Folded: "243", }, Tags: TagsConfig{ Enabled: true, DefaultTag: "work", Tags: []TagConfig{ {Name: "work", Color: "99"}, {Name: "personal", Color: "141"}, {Name: "urgent", Color: "196"}, {Name: "important", Color: "220"}, }, }, States: StatesConfig{ States: []StateConfig{ {Name: "TODO", Color: "202"}, {Name: "PROG", Color: "220"}, {Name: "BLOCK", Color: "196"}, {Name: "DONE", Color: "34"}, }, DefaultNewTaskState: "TODO", }, UI: UIConfig{ HelpTextWidth: 22, MinTerminalWidth: 40, AgendaDays: 7, }, } } // GetConfigPath returns the path to the config file func GetConfigPath() (string, error) { configDir, err := os.UserConfigDir() if err != nil { return "", fmt.Errorf("failed to get config directory: %w", err) } orgConfigDir := filepath.Join(configDir, "org") configPath := filepath.Join(orgConfigDir, "config.toml") return configPath, nil } // LoadConfig loads the configuration from the config file func LoadConfig() (*Config, error) { configPath, err := GetConfigPath() if err != nil { return nil, err } // If config file doesn't exist, create it with defaults if _, err := os.Stat(configPath); os.IsNotExist(err) { defaultCfg := DefaultConfig() if err := defaultCfg.Save(); err != nil { // If we can't save, just return defaults return defaultCfg, nil } return defaultCfg, nil } var config Config if _, err := toml.DecodeFile(configPath, &config); err != nil { return nil, fmt.Errorf("failed to parse config file: %w", err) } // Merge with defaults for any missing values config.fillDefaults() return &config, nil } // Save saves the configuration to the config file func (c *Config) Save() error { configPath, err := GetConfigPath() if err != nil { return err } // Ensure config directory exists configDir := filepath.Dir(configPath) if err := os.MkdirAll(configDir, 0755); err != nil { return fmt.Errorf("failed to create config directory: %w", err) } // Create or truncate the file f, err := os.Create(configPath) if err != nil { return fmt.Errorf("failed to create config file: %w", err) } defer f.Close() // Encode config to TOML encoder := toml.NewEncoder(f) if err := encoder.Encode(c); err != nil { return fmt.Errorf("failed to write config: %w", err) } return nil } // fillDefaults fills in any missing config values with defaults func (c *Config) fillDefaults() { defaults := DefaultConfig() // Fill keybindings if empty if len(c.Keybindings.Up) == 0 { c.Keybindings.Up = defaults.Keybindings.Up } if len(c.Keybindings.Down) == 0 { c.Keybindings.Down = defaults.Keybindings.Down } if len(c.Keybindings.Left) == 0 { c.Keybindings.Left = defaults.Keybindings.Left } if len(c.Keybindings.Right) == 0 { c.Keybindings.Right = defaults.Keybindings.Right } if len(c.Keybindings.ShiftUp) == 0 { c.Keybindings.ShiftUp = defaults.Keybindings.ShiftUp } if len(c.Keybindings.ShiftDown) == 0 { c.Keybindings.ShiftDown = defaults.Keybindings.ShiftDown } if len(c.Keybindings.ShiftLeft) == 0 { c.Keybindings.ShiftLeft = defaults.Keybindings.ShiftLeft } if len(c.Keybindings.ShiftRight) == 0 { c.Keybindings.ShiftRight = defaults.Keybindings.ShiftRight } if len(c.Keybindings.Rename) == 0 { c.Keybindings.Rename = defaults.Keybindings.Rename } if len(c.Keybindings.CycleState) == 0 { c.Keybindings.CycleState = defaults.Keybindings.CycleState } if len(c.Keybindings.ToggleFold) == 0 { c.Keybindings.ToggleFold = defaults.Keybindings.ToggleFold } if len(c.Keybindings.EditNotes) == 0 { c.Keybindings.EditNotes = defaults.Keybindings.EditNotes } if len(c.Keybindings.ToggleView) == 0 { c.Keybindings.ToggleView = defaults.Keybindings.ToggleView } if len(c.Keybindings.Capture) == 0 { c.Keybindings.Capture = defaults.Keybindings.Capture } if len(c.Keybindings.AddSubTask) == 0 { c.Keybindings.AddSubTask = defaults.Keybindings.AddSubTask } if len(c.Keybindings.Delete) == 0 { c.Keybindings.Delete = defaults.Keybindings.Delete } if len(c.Keybindings.Save) == 0 { c.Keybindings.Save = defaults.Keybindings.Save } if len(c.Keybindings.ToggleReorder) == 0 { c.Keybindings.ToggleReorder = defaults.Keybindings.ToggleReorder } if len(c.Keybindings.ClockIn) == 0 { c.Keybindings.ClockIn = defaults.Keybindings.ClockIn } if len(c.Keybindings.ClockOut) == 0 { c.Keybindings.ClockOut = defaults.Keybindings.ClockOut } if len(c.Keybindings.SetDeadline) == 0 { c.Keybindings.SetDeadline = defaults.Keybindings.SetDeadline } if len(c.Keybindings.SetPriority) == 0 { c.Keybindings.SetPriority = defaults.Keybindings.SetPriority } if len(c.Keybindings.SetEffort) == 0 { c.Keybindings.SetEffort = defaults.Keybindings.SetEffort } if len(c.Keybindings.Help) == 0 { c.Keybindings.Help = defaults.Keybindings.Help } if len(c.Keybindings.Quit) == 0 { c.Keybindings.Quit = defaults.Keybindings.Quit } if len(c.Keybindings.Settings) == 0 { c.Keybindings.Settings = defaults.Keybindings.Settings } if len(c.Keybindings.TagItem) == 0 { c.Keybindings.TagItem = defaults.Keybindings.TagItem } // Fill colors if empty if c.Colors.Todo == "" { c.Colors.Todo = defaults.Colors.Todo } if c.Colors.Progress == "" { c.Colors.Progress = defaults.Colors.Progress } if c.Colors.Blocked == "" { c.Colors.Blocked = defaults.Colors.Blocked } if c.Colors.Done == "" { c.Colors.Done = defaults.Colors.Done } if c.Colors.Cursor == "" { c.Colors.Cursor = defaults.Colors.Cursor } if c.Colors.Title == "" { c.Colors.Title = defaults.Colors.Title } if c.Colors.Scheduled == "" { c.Colors.Scheduled = defaults.Colors.Scheduled } if c.Colors.Overdue == "" { c.Colors.Overdue = defaults.Colors.Overdue } if c.Colors.Status == "" { c.Colors.Status = defaults.Colors.Status } if c.Colors.Note == "" { c.Colors.Note = defaults.Colors.Note } if c.Colors.Folded == "" { c.Colors.Folded = defaults.Colors.Folded } // Fill tags if empty if len(c.Tags.Tags) == 0 { c.Tags.Tags = defaults.Tags.Tags } if c.Tags.DefaultTag == "" { c.Tags.DefaultTag = defaults.Tags.DefaultTag } // Fill states if empty if len(c.States.States) == 0 { c.States.States = defaults.States.States } if c.States.DefaultNewTaskState == "" { c.States.DefaultNewTaskState = defaults.States.DefaultNewTaskState } // Fill UI if zero values if c.UI.HelpTextWidth == 0 { c.UI.HelpTextWidth = defaults.UI.HelpTextWidth } if c.UI.MinTerminalWidth == 0 { c.UI.MinTerminalWidth = defaults.UI.MinTerminalWidth } if c.UI.AgendaDays == 0 { c.UI.AgendaDays = defaults.UI.AgendaDays } } // BuildKeyBinding creates a key.Binding from config func BuildKeyBinding(keys []string, help string, description string) key.Binding { return key.NewBinding( key.WithKeys(keys...), key.WithHelp(help, description), ) } // GetTagColor returns the color for a given tag name func (c *Config) GetTagColor(tagName string) string { for _, tag := range c.Tags.Tags { if tag.Name == tagName { return tag.Color } } // Return a default color if tag not found return "99" } // AddTag adds a new tag to the configuration func (c *Config) AddTag(name, color string) { // Check if tag already exists for i, tag := range c.Tags.Tags { if tag.Name == name { c.Tags.Tags[i].Color = color return } } c.Tags.Tags = append(c.Tags.Tags, TagConfig{Name: name, Color: color}) } // RemoveTag removes a tag from the configuration func (c *Config) RemoveTag(name string) { for i, tag := range c.Tags.Tags { if tag.Name == name { c.Tags.Tags = append(c.Tags.Tags[:i], c.Tags.Tags[i+1:]...) return } } } // UpdateTagColor updates the color of an existing tag func (c *Config) UpdateTagColor(name, color string) { for i, tag := range c.Tags.Tags { if tag.Name == name { c.Tags.Tags[i].Color = color return } } } // GetStateColor returns the color for a given state name func (c *Config) GetStateColor(stateName string) string { for _, state := range c.States.States { if state.Name == stateName { return state.Color } } // Return a default color if state not found return "99" } // AddState adds a new state to the configuration func (c *Config) AddState(name, color string) { // Check if state already exists for i, state := range c.States.States { if state.Name == name { c.States.States[i].Color = color return } } c.States.States = append(c.States.States, StateConfig{Name: name, Color: color}) } // RemoveState removes a state from the configuration func (c *Config) RemoveState(name string) { for i, state := range c.States.States { if state.Name == name { c.States.States = append(c.States.States[:i], c.States.States[i+1:]...) return } } } // UpdateStateColor updates the color of an existing state func (c *Config) UpdateStateColor(name, color string) { for i, state := range c.States.States { if state.Name == name { c.States.States[i].Color = color return } } } // GetStateNames returns all configured state names func (c *Config) GetStateNames() []string { names := make([]string, len(c.States.States)) for i, state := range c.States.States { names[i] = state.Name } return names } // 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 switch action { case "up": c.Keybindings.Up = keys case "down": c.Keybindings.Down = keys case "left": c.Keybindings.Left = keys case "right": c.Keybindings.Right = keys case "cycle_state": c.Keybindings.CycleState = keys case "toggle_fold": c.Keybindings.ToggleFold = keys case "edit_notes": c.Keybindings.EditNotes = keys case "capture": c.Keybindings.Capture = keys case "add_subtask": c.Keybindings.AddSubTask = keys case "delete": c.Keybindings.Delete = keys case "tag_item": c.Keybindings.TagItem = keys case "settings": c.Keybindings.Settings = keys case "toggle_view": c.Keybindings.ToggleView = keys case "save": c.Keybindings.Save = keys case "help": c.Keybindings.Help = keys case "quit": c.Keybindings.Quit = keys default: return fmt.Errorf("unknown action: %s", action) } return nil } // GetAllKeybindings returns a map of all keybindings func (c *Config) GetAllKeybindings() map[string][]string { return map[string][]string{ "up": c.Keybindings.Up, "down": c.Keybindings.Down, "left": c.Keybindings.Left, "right": c.Keybindings.Right, "shift_up": c.Keybindings.ShiftUp, "shift_down": c.Keybindings.ShiftDown, "shift_left": c.Keybindings.ShiftLeft, "shift_right": c.Keybindings.ShiftRight, "rename": c.Keybindings.Rename, "cycle_state": c.Keybindings.CycleState, "toggle_fold": c.Keybindings.ToggleFold, "edit_notes": c.Keybindings.EditNotes, "toggle_view": c.Keybindings.ToggleView, "capture": c.Keybindings.Capture, "add_subtask": c.Keybindings.AddSubTask, "delete": c.Keybindings.Delete, "save": c.Keybindings.Save, "toggle_reorder": c.Keybindings.ToggleReorder, "clock_in": c.Keybindings.ClockIn, "clock_out": c.Keybindings.ClockOut, "set_deadline": c.Keybindings.SetDeadline, "set_priority": c.Keybindings.SetPriority, "set_effort": c.Keybindings.SetEffort, "help": c.Keybindings.Help, "quit": c.Keybindings.Quit, "settings": c.Keybindings.Settings, "tag_item": c.Keybindings.TagItem, } } // GetDefaultNewTaskState returns the default state for new tasks // Returns empty string if configured as "none" or if the configured state doesn't exist func (c *Config) GetDefaultNewTaskState() string { // Empty string means no state if c.States.DefaultNewTaskState == "" { return "" } // Validate that the configured state exists for _, state := range c.States.States { if state.Name == c.States.DefaultNewTaskState { return c.States.DefaultNewTaskState } } // If configured state doesn't exist, fall back to first state or empty if len(c.States.States) > 0 { return c.States.States[0].Name } return "" }