Files
gems/opal-task/internal/engine/config.go
T
joakim 8f6db4672a Implement urgency system with TaskWarrior-inspired calculation
- Add urgency calculation based on multiple factors:
  * Due date (linear scale: overdue=12.0, today=10.0, week=6.0, 2weeks=2.0)
  * Priority (H=6.0, M=3.9, D=1.8, L=0.0)
  * Age (0-2.0 over 365 days)
  * Active status (+4.0 boost)
  * Waiting status (-3.0 penalty)
  * Tags (+1.0 with count modifier)
  * Project assignment (+1.0)
  * Configurable urgent tag (default 'next', +15.0)

- Replace priority column with urgency in all reports
  * Display as decimal with 1 decimal place
  * 4-tier color coding: ≥10 (bright red), ≥5 (red), ≥2 (yellow), <2 (cyan)
  * Minimal format color-coded by urgency

- Add default urgency sorting to all reports
  * list, minimal, active, ready, overdue reports sort by urgency
  * newest/oldest keep date-based sorting

- Implement 'next' report
  * Shows most urgent ready tasks
  * Configurable limit (default 5)
  * Only includes tasks ready to work on (no future wait/scheduled)

- Add urgency display to info command
  * Shows urgency score alongside priority

- All urgency coefficients configurable via config
  * Adjusted defaults for Opal's simpler model (no blocking/annotations)
  * Configurable urgent tag name (not hardcoded to 'next')

Priority order maintained: High > Medium > Default > Low
2026-01-06 14:32:44 +01:00

220 lines
6.9 KiB
Go

package engine
import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/spf13/viper"
)
type Config struct {
DefaultFilter string `mapstructure:"default_filter"`
DefaultSort string `mapstructure:"default_sort"`
DefaultReport string `mapstructure:"default_report"`
ColorOutput bool `mapstructure:"color_output"`
WeekStartDay string `mapstructure:"week_start_day"`
DefaultDueTime string `mapstructure:"default_due_time"`
// Urgency coefficients
UrgencyDue float64 `mapstructure:"urgency_due_coefficient"`
UrgencyPriorityH float64 `mapstructure:"urgency_priority_h_coefficient"`
UrgencyPriorityM float64 `mapstructure:"urgency_priority_m_coefficient"`
UrgencyPriorityD float64 `mapstructure:"urgency_priority_d_coefficient"`
UrgencyPriorityL float64 `mapstructure:"urgency_priority_l_coefficient"`
UrgencyActive float64 `mapstructure:"urgency_active_coefficient"`
UrgencyAge float64 `mapstructure:"urgency_age_coefficient"`
UrgencyAgeMax int `mapstructure:"urgency_age_max"`
UrgencyTags float64 `mapstructure:"urgency_tags_coefficient"`
UrgencyProject float64 `mapstructure:"urgency_project_coefficient"`
UrgencyWaiting float64 `mapstructure:"urgency_waiting_coefficient"`
UrgencyUrgentTag string `mapstructure:"urgency_urgent_tag"`
UrgencyUrgentCoeff float64 `mapstructure:"urgency_urgent_coefficient"`
NextLimit int `mapstructure:"next_limit"`
// Sync settings
SyncEnabled bool `mapstructure:"sync_enabled"`
SyncURL string `mapstructure:"sync_url"`
SyncAPIKey string `mapstructure:"sync_api_key"`
SyncClientID string `mapstructure:"sync_client_id"`
SyncStrategy string `mapstructure:"sync_strategy"`
SyncQueueOffline bool `mapstructure:"sync_queue_offline"`
}
var globalConfig *Config
// GetConfigDir returns the configuration directory path
func GetConfigDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to get home directory: %w", err)
}
return filepath.Join(home, ".config", "jade"), nil
}
// GetDBPath returns the path to the SQLite database
func GetDBPath() (string, error) {
configDir, err := GetConfigDir()
if err != nil {
return "", err
}
return filepath.Join(configDir, "opal.db"), nil
}
// GetConfigPath returns the path to the config file
func GetConfigPath() (string, error) {
configDir, err := GetConfigDir()
if err != nil {
return "", err
}
return filepath.Join(configDir, "opal.yml"), nil
}
// LoadConfig loads the configuration from file or creates default
func LoadConfig() (*Config, error) {
if globalConfig != nil {
return globalConfig, nil
}
configDir, err := GetConfigDir()
if err != nil {
return nil, err
}
// Ensure config directory exists
if err := os.MkdirAll(configDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create config directory: %w", err)
}
configPath, err := GetConfigPath()
if err != nil {
return nil, err
}
v := viper.New()
v.SetConfigFile(configPath)
v.SetConfigType("yaml")
// Set defaults
v.SetDefault("default_filter", "status:pending")
v.SetDefault("default_sort", "due,priority")
v.SetDefault("default_report", "list")
v.SetDefault("color_output", true)
v.SetDefault("week_start_day", "monday")
v.SetDefault("default_due_time", "")
// Urgency defaults (adjusted for Opal's simpler model)
v.SetDefault("urgency_due_coefficient", 12.0)
v.SetDefault("urgency_priority_h_coefficient", 6.0)
v.SetDefault("urgency_priority_m_coefficient", 3.9)
v.SetDefault("urgency_priority_d_coefficient", 1.8)
v.SetDefault("urgency_priority_l_coefficient", 0.0)
v.SetDefault("urgency_active_coefficient", 4.0)
v.SetDefault("urgency_age_coefficient", 2.0)
v.SetDefault("urgency_age_max", 365)
v.SetDefault("urgency_tags_coefficient", 1.0)
v.SetDefault("urgency_project_coefficient", 1.0)
v.SetDefault("urgency_waiting_coefficient", -3.0)
v.SetDefault("urgency_urgent_tag", "next")
v.SetDefault("urgency_urgent_coefficient", 15.0)
v.SetDefault("next_limit", 5)
// Sync defaults
v.SetDefault("sync_enabled", false)
v.SetDefault("sync_url", "")
v.SetDefault("sync_api_key", "")
v.SetDefault("sync_client_id", "")
v.SetDefault("sync_strategy", "last-write-wins")
v.SetDefault("sync_queue_offline", true)
// Try to read existing config
err = v.ReadInConfig()
if err != nil {
// Config doesn't exist, create it with defaults
// Write config with defaults (ignoring the error type check for now)
if err := v.WriteConfigAs(configPath); err != nil {
return nil, fmt.Errorf("failed to create config file: %w", err)
}
// Try reading again
if err := v.ReadInConfig(); err != nil {
return nil, fmt.Errorf("failed to read newly created config: %w", err)
}
}
cfg := &Config{}
if err := v.Unmarshal(cfg); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
globalConfig = cfg
return cfg, nil
}
// SaveConfig saves the configuration to file
func SaveConfig(cfg *Config) error {
configPath, err := GetConfigPath()
if err != nil {
return err
}
v := viper.New()
v.SetConfigFile(configPath)
v.SetConfigType("yaml")
v.Set("default_filter", cfg.DefaultFilter)
v.Set("default_sort", cfg.DefaultSort)
v.Set("default_report", cfg.DefaultReport)
v.Set("color_output", cfg.ColorOutput)
v.Set("week_start_day", cfg.WeekStartDay)
v.Set("default_due_time", cfg.DefaultDueTime)
// Urgency settings
v.Set("urgency_due_coefficient", cfg.UrgencyDue)
v.Set("urgency_priority_h_coefficient", cfg.UrgencyPriorityH)
v.Set("urgency_priority_m_coefficient", cfg.UrgencyPriorityM)
v.Set("urgency_priority_d_coefficient", cfg.UrgencyPriorityD)
v.Set("urgency_priority_l_coefficient", cfg.UrgencyPriorityL)
v.Set("urgency_active_coefficient", cfg.UrgencyActive)
v.Set("urgency_age_coefficient", cfg.UrgencyAge)
v.Set("urgency_age_max", cfg.UrgencyAgeMax)
v.Set("urgency_tags_coefficient", cfg.UrgencyTags)
v.Set("urgency_project_coefficient", cfg.UrgencyProject)
v.Set("urgency_waiting_coefficient", cfg.UrgencyWaiting)
v.Set("urgency_urgent_tag", cfg.UrgencyUrgentTag)
v.Set("urgency_urgent_coefficient", cfg.UrgencyUrgentCoeff)
v.Set("next_limit", cfg.NextLimit)
// Sync settings
v.Set("sync_enabled", cfg.SyncEnabled)
v.Set("sync_url", cfg.SyncURL)
v.Set("sync_api_key", cfg.SyncAPIKey)
v.Set("sync_client_id", cfg.SyncClientID)
v.Set("sync_strategy", cfg.SyncStrategy)
v.Set("sync_queue_offline", cfg.SyncQueueOffline)
return v.WriteConfig()
}
// GetConfig returns the loaded config or loads it if not already loaded
func GetConfig() (*Config, error) {
if globalConfig != nil {
return globalConfig, nil
}
return LoadConfig()
}
// GetWeekStart returns the configured week start day
func (c *Config) GetWeekStart() time.Weekday {
if strings.ToLower(c.WeekStartDay) == "sunday" {
return time.Sunday
}
return time.Monday // default
}
// GetDefaultDueTime returns the configured default due time (HH:MM format or empty)
func (c *Config) GetDefaultDueTime() string {
return c.DefaultDueTime
}