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
This commit is contained in:
@@ -31,7 +31,7 @@ var commandNames = []string{
|
|||||||
// Report names (dynamically populated)
|
// Report names (dynamically populated)
|
||||||
var reportNames = []string{
|
var reportNames = []string{
|
||||||
"active", "all", "completed", "list", "minimal",
|
"active", "all", "completed", "list", "minimal",
|
||||||
"newest", "oldest", "overdue", "ready", "recurring",
|
"newest", "next", "oldest", "overdue", "ready", "recurring",
|
||||||
"template", "waiting",
|
"template", "waiting",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,22 @@ type Config struct {
|
|||||||
WeekStartDay string `mapstructure:"week_start_day"`
|
WeekStartDay string `mapstructure:"week_start_day"`
|
||||||
DefaultDueTime string `mapstructure:"default_due_time"`
|
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
|
// Sync settings
|
||||||
SyncEnabled bool `mapstructure:"sync_enabled"`
|
SyncEnabled bool `mapstructure:"sync_enabled"`
|
||||||
SyncURL string `mapstructure:"sync_url"`
|
SyncURL string `mapstructure:"sync_url"`
|
||||||
@@ -89,6 +105,22 @@ func LoadConfig() (*Config, error) {
|
|||||||
v.SetDefault("week_start_day", "monday")
|
v.SetDefault("week_start_day", "monday")
|
||||||
v.SetDefault("default_due_time", "")
|
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
|
// Sync defaults
|
||||||
v.SetDefault("sync_enabled", false)
|
v.SetDefault("sync_enabled", false)
|
||||||
v.SetDefault("sync_url", "")
|
v.SetDefault("sync_url", "")
|
||||||
@@ -138,6 +170,22 @@ func SaveConfig(cfg *Config) error {
|
|||||||
v.Set("week_start_day", cfg.WeekStartDay)
|
v.Set("week_start_day", cfg.WeekStartDay)
|
||||||
v.Set("default_due_time", cfg.DefaultDueTime)
|
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
|
// Sync settings
|
||||||
v.Set("sync_enabled", cfg.SyncEnabled)
|
v.Set("sync_enabled", cfg.SyncEnabled)
|
||||||
v.Set("sync_url", cfg.SyncURL)
|
v.Set("sync_url", cfg.SyncURL)
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ func FormatTaskListWithFormat(tasks []*Task, ws *WorkingSet, format string) stri
|
|||||||
return "No tasks found."
|
return "No tasks found."
|
||||||
}
|
}
|
||||||
|
|
||||||
// Minimal format: just ID and description
|
// Get urgency coefficients
|
||||||
|
cfg, _ := GetConfig()
|
||||||
|
coeffs := BuildUrgencyCoefficients(cfg)
|
||||||
|
|
||||||
|
// Minimal format: just ID and description, color-coded by urgency
|
||||||
if format == "minimal" {
|
if format == "minimal" {
|
||||||
result := ""
|
result := ""
|
||||||
for i, task := range tasks {
|
for i, task := range tasks {
|
||||||
@@ -34,7 +38,9 @@ func FormatTaskListWithFormat(tasks []*Task, ws *WorkingSet, format string) stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result += fmt.Sprintf("%3d %s\n", displayID, task.Description)
|
urgency := task.CalculateUrgency(coeffs)
|
||||||
|
urgencyColor := getUrgencyColor(urgency)
|
||||||
|
result += urgencyColor.Sprintf("%3d %s\n", displayID, task.Description)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -51,7 +57,7 @@ func FormatTaskListWithFormat(tasks []*Task, ws *WorkingSet, format string) stri
|
|||||||
t.SetColumnConfigs([]table.ColumnConfig{
|
t.SetColumnConfigs([]table.ColumnConfig{
|
||||||
{Number: 1, WidthMin: 3, WidthMax: 3, Align: text.AlignRight}, // ID
|
{Number: 1, WidthMin: 3, WidthMax: 3, Align: text.AlignRight}, // ID
|
||||||
{Number: 2, WidthMin: 8, WidthMax: 8, Align: text.AlignLeft}, // Status
|
{Number: 2, WidthMin: 8, WidthMax: 8, Align: text.AlignLeft}, // Status
|
||||||
{Number: 3, WidthMin: 3, WidthMax: 3, Align: text.AlignCenter}, // Pri
|
{Number: 3, WidthMin: 4, WidthMax: 4, Align: text.AlignRight}, // Urg
|
||||||
{Number: 4, WidthMin: 12, WidthMax: 12, Align: text.AlignLeft}, // Project
|
{Number: 4, WidthMin: 12, WidthMax: 12, Align: text.AlignLeft}, // Project
|
||||||
{Number: 5, WidthMin: 40, WidthMax: 40, Align: text.AlignLeft, // Description
|
{Number: 5, WidthMin: 40, WidthMax: 40, Align: text.AlignLeft, // Description
|
||||||
WidthMaxEnforcer: text.Trim},
|
WidthMaxEnforcer: text.Trim},
|
||||||
@@ -60,7 +66,7 @@ func FormatTaskListWithFormat(tasks []*Task, ws *WorkingSet, format string) stri
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Add header
|
// Add header
|
||||||
t.AppendHeader(table.Row{"ID", "Status", "Pri", "Project", "Description", "Due", "Tags"})
|
t.AppendHeader(table.Row{"ID", "Status", "Urg", "Project", "Description", "Due", "Tags"})
|
||||||
|
|
||||||
// Add rows
|
// Add rows
|
||||||
for i, task := range tasks {
|
for i, task := range tasks {
|
||||||
@@ -75,10 +81,11 @@ func FormatTaskListWithFormat(tasks []*Task, ws *WorkingSet, format string) stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
urgency := task.CalculateUrgency(coeffs)
|
||||||
t.AppendRow(table.Row{
|
t.AppendRow(table.Row{
|
||||||
displayID,
|
displayID,
|
||||||
formatStatus(task.Status),
|
formatStatus(task.Status),
|
||||||
formatPriority(task.Priority),
|
formatUrgency(urgency),
|
||||||
formatProject(task.Project),
|
formatProject(task.Project),
|
||||||
task.Description,
|
task.Description,
|
||||||
formatDue(task.Due),
|
formatDue(task.Due),
|
||||||
@@ -108,10 +115,16 @@ func FormatTaskDetail(task *Task) string {
|
|||||||
// Add title as header
|
// Add title as header
|
||||||
t.SetTitle(color.New(color.Bold).Sprint("Task Details"))
|
t.SetTitle(color.New(color.Bold).Sprint("Task Details"))
|
||||||
|
|
||||||
|
// Calculate urgency
|
||||||
|
cfg, _ := GetConfig()
|
||||||
|
coeffs := BuildUrgencyCoefficients(cfg)
|
||||||
|
urgency := task.CalculateUrgency(coeffs)
|
||||||
|
|
||||||
// Core fields
|
// Core fields
|
||||||
t.AppendRow(table.Row{"UUID", task.UUID})
|
t.AppendRow(table.Row{"UUID", task.UUID})
|
||||||
t.AppendRow(table.Row{"Status", formatStatus(task.Status)})
|
t.AppendRow(table.Row{"Status", formatStatus(task.Status)})
|
||||||
t.AppendRow(table.Row{"Description", task.Description})
|
t.AppendRow(table.Row{"Description", task.Description})
|
||||||
|
t.AppendRow(table.Row{"Urgency", formatUrgency(urgency)})
|
||||||
t.AppendRow(table.Row{"Priority", formatPriority(task.Priority)})
|
t.AppendRow(table.Row{"Priority", formatPriority(task.Priority)})
|
||||||
t.AppendRow(table.Row{"Project", formatProject(task.Project)})
|
t.AppendRow(table.Row{"Project", formatProject(task.Project)})
|
||||||
|
|
||||||
@@ -253,6 +266,38 @@ func formatPriority(priority Priority) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func formatUrgency(urgency float64) string {
|
||||||
|
formatted := fmt.Sprintf("%.1f", urgency)
|
||||||
|
|
||||||
|
// 4-tier color coding
|
||||||
|
if urgency >= 10.0 {
|
||||||
|
// Critical urgency - bright red
|
||||||
|
return color.New(color.FgHiRed, color.Bold).Sprint(formatted)
|
||||||
|
} else if urgency >= 5.0 {
|
||||||
|
// High urgency - red
|
||||||
|
return color.RedString(formatted)
|
||||||
|
} else if urgency >= 2.0 {
|
||||||
|
// Medium urgency - yellow
|
||||||
|
return color.YellowString(formatted)
|
||||||
|
} else {
|
||||||
|
// Low urgency - cyan
|
||||||
|
return color.CyanString(formatted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUrgencyColor(urgency float64) *color.Color {
|
||||||
|
// Returns color for minimal format
|
||||||
|
if urgency >= 10.0 {
|
||||||
|
return color.New(color.FgHiRed, color.Bold)
|
||||||
|
} else if urgency >= 5.0 {
|
||||||
|
return color.New(color.FgRed)
|
||||||
|
} else if urgency >= 2.0 {
|
||||||
|
return color.New(color.FgYellow)
|
||||||
|
} else {
|
||||||
|
return color.New(color.FgCyan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func formatProject(project *string) string {
|
func formatProject(project *string) string {
|
||||||
if project == nil {
|
if project == nil {
|
||||||
return "-"
|
return "-"
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ func AllReports() map[string]*Report {
|
|||||||
"list": ListReport(),
|
"list": ListReport(),
|
||||||
"minimal": MinimalReport(),
|
"minimal": MinimalReport(),
|
||||||
"newest": NewestReport(),
|
"newest": NewestReport(),
|
||||||
|
"next": NextReport(),
|
||||||
"oldest": OldestReport(),
|
"oldest": OldestReport(),
|
||||||
"overdue": OverdueReport(),
|
"overdue": OverdueReport(),
|
||||||
"ready": ReadyReport(),
|
"ready": ReadyReport(),
|
||||||
@@ -61,6 +62,7 @@ func ActiveReport() *Report {
|
|||||||
Description: "Started tasks",
|
Description: "Started tasks",
|
||||||
BaseFilter: filter,
|
BaseFilter: filter,
|
||||||
DisplayFormat: DisplayFormatTable,
|
DisplayFormat: DisplayFormatTable,
|
||||||
|
SortFunc: sortByUrgency,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +102,7 @@ func ListReport() *Report {
|
|||||||
Description: "Pending tasks",
|
Description: "Pending tasks",
|
||||||
BaseFilter: filter,
|
BaseFilter: filter,
|
||||||
DisplayFormat: DisplayFormatTable,
|
DisplayFormat: DisplayFormatTable,
|
||||||
|
SortFunc: sortByUrgency,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +116,7 @@ func MinimalReport() *Report {
|
|||||||
Description: "Pending tasks (minimal format)",
|
Description: "Pending tasks (minimal format)",
|
||||||
BaseFilter: filter,
|
BaseFilter: filter,
|
||||||
DisplayFormat: DisplayFormatMinimal,
|
DisplayFormat: DisplayFormatMinimal,
|
||||||
|
SortFunc: sortByUrgency,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +152,51 @@ func NewestReport() *Report {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NextReport shows most urgent tasks ready to work on
|
||||||
|
func NextReport() *Report {
|
||||||
|
filter := NewFilter()
|
||||||
|
filter.Attributes["status"] = "pending"
|
||||||
|
filter.Attributes["_ready"] = "true"
|
||||||
|
|
||||||
|
return &Report{
|
||||||
|
Name: "next",
|
||||||
|
Description: "Most urgent tasks ready to work on",
|
||||||
|
BaseFilter: filter,
|
||||||
|
DisplayFormat: DisplayFormatTable,
|
||||||
|
SortFunc: func(tasks []*Task) []*Task {
|
||||||
|
// Sort by urgency descending
|
||||||
|
cfg, _ := GetConfig()
|
||||||
|
coeffs := BuildUrgencyCoefficients(cfg)
|
||||||
|
|
||||||
|
sorted := make([]*Task, len(tasks))
|
||||||
|
copy(sorted, tasks)
|
||||||
|
|
||||||
|
for i := 0; i < len(sorted)-1; i++ {
|
||||||
|
for j := i + 1; j < len(sorted); j++ {
|
||||||
|
urgI := sorted[i].CalculateUrgency(coeffs)
|
||||||
|
urgJ := sorted[j].CalculateUrgency(coeffs)
|
||||||
|
if urgI < urgJ {
|
||||||
|
sorted[i], sorted[j] = sorted[j], sorted[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sorted
|
||||||
|
},
|
||||||
|
LimitFunc: func(tasks []*Task) []*Task {
|
||||||
|
cfg, _ := GetConfig()
|
||||||
|
limit := cfg.NextLimit
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 5
|
||||||
|
}
|
||||||
|
if len(tasks) > limit {
|
||||||
|
return tasks[:limit]
|
||||||
|
}
|
||||||
|
return tasks
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// OldestReport shows oldest pending tasks
|
// OldestReport shows oldest pending tasks
|
||||||
func OldestReport() *Report {
|
func OldestReport() *Report {
|
||||||
filter := NewFilter()
|
filter := NewFilter()
|
||||||
@@ -185,6 +234,7 @@ func OverdueReport() *Report {
|
|||||||
Description: "Overdue tasks",
|
Description: "Overdue tasks",
|
||||||
BaseFilter: filter,
|
BaseFilter: filter,
|
||||||
DisplayFormat: DisplayFormatTable,
|
DisplayFormat: DisplayFormatTable,
|
||||||
|
SortFunc: sortByUrgency,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +249,7 @@ func ReadyReport() *Report {
|
|||||||
Description: "Tasks ready to work on",
|
Description: "Tasks ready to work on",
|
||||||
BaseFilter: filter,
|
BaseFilter: filter,
|
||||||
DisplayFormat: DisplayFormatTable,
|
DisplayFormat: DisplayFormatTable,
|
||||||
|
SortFunc: sortByUrgency,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,3 +419,24 @@ func mergeFilters(base, user *Filter) *Filter {
|
|||||||
|
|
||||||
return merged
|
return merged
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sortByUrgency is a helper function to sort tasks by urgency (descending)
|
||||||
|
func sortByUrgency(tasks []*Task) []*Task {
|
||||||
|
cfg, _ := GetConfig()
|
||||||
|
coeffs := BuildUrgencyCoefficients(cfg)
|
||||||
|
|
||||||
|
sorted := make([]*Task, len(tasks))
|
||||||
|
copy(sorted, tasks)
|
||||||
|
|
||||||
|
for i := 0; i < len(sorted)-1; i++ {
|
||||||
|
for j := i + 1; j < len(sorted); j++ {
|
||||||
|
urgI := sorted[i].CalculateUrgency(coeffs)
|
||||||
|
urgJ := sorted[j].CalculateUrgency(coeffs)
|
||||||
|
if urgI < urgJ {
|
||||||
|
sorted[i], sorted[j] = sorted[j], sorted[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sorted
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
// UrgencyCoefficients holds the configurable coefficients for urgency calculation
|
||||||
|
type UrgencyCoefficients struct {
|
||||||
|
Due float64
|
||||||
|
PriorityH float64
|
||||||
|
PriorityM float64
|
||||||
|
PriorityD float64
|
||||||
|
PriorityL float64
|
||||||
|
Active float64
|
||||||
|
Age float64
|
||||||
|
AgeMax int
|
||||||
|
Tags float64
|
||||||
|
Project float64
|
||||||
|
Waiting float64
|
||||||
|
UrgentTag string // Configurable urgent tag name (default: "next")
|
||||||
|
UrgentCoeff float64 // Coefficient for urgent tag
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateUrgency computes the urgency score for a task
|
||||||
|
func (t *Task) CalculateUrgency(coeffs *UrgencyCoefficients) float64 {
|
||||||
|
urgency := 0.0
|
||||||
|
|
||||||
|
// Due date contribution
|
||||||
|
urgency += t.urgencyDue(coeffs.Due)
|
||||||
|
|
||||||
|
// Priority contribution
|
||||||
|
urgency += t.urgencyPriority(coeffs)
|
||||||
|
|
||||||
|
// Age contribution
|
||||||
|
urgency += t.urgencyAge(coeffs.Age, coeffs.AgeMax)
|
||||||
|
|
||||||
|
// Active task contribution
|
||||||
|
urgency += t.urgencyActive(coeffs.Active)
|
||||||
|
|
||||||
|
// Waiting task penalty
|
||||||
|
urgency += t.urgencyWaiting(coeffs.Waiting)
|
||||||
|
|
||||||
|
// Tags contribution
|
||||||
|
urgency += t.urgencyTags(coeffs)
|
||||||
|
|
||||||
|
// Project contribution
|
||||||
|
urgency += t.urgencyProject(coeffs.Project)
|
||||||
|
|
||||||
|
return urgency
|
||||||
|
}
|
||||||
|
|
||||||
|
// urgencyDue calculates urgency contribution from due date
|
||||||
|
// Linear scale: overdue=12.0, today=10.0, 7 days=6.0, 14+ days=2.0
|
||||||
|
func (t *Task) urgencyDue(coeff float64) float64 {
|
||||||
|
if t.Due == nil {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
now := timeNow()
|
||||||
|
due := *t.Due
|
||||||
|
|
||||||
|
// Calculate days until due (negative if overdue)
|
||||||
|
duration := due.Sub(now)
|
||||||
|
daysUntil := duration.Hours() / 24.0
|
||||||
|
|
||||||
|
var scale float64
|
||||||
|
|
||||||
|
if daysUntil < 0 {
|
||||||
|
// Overdue - maximum urgency
|
||||||
|
scale = 12.0
|
||||||
|
} else if daysUntil < 1 {
|
||||||
|
// Due today
|
||||||
|
scale = 10.0
|
||||||
|
} else if daysUntil <= 7 {
|
||||||
|
// Due within a week - linear from 10.0 to 6.0
|
||||||
|
scale = 10.0 - (daysUntil * 0.571) // (10-6)/7 = 0.571
|
||||||
|
} else if daysUntil <= 14 {
|
||||||
|
// Due within two weeks - linear from 6.0 to 2.0
|
||||||
|
scale = 6.0 - ((daysUntil - 7.0) * 0.571) // (6-2)/7 = 0.571
|
||||||
|
} else {
|
||||||
|
// Due further out - low urgency
|
||||||
|
scale = 2.0
|
||||||
|
}
|
||||||
|
|
||||||
|
return scale * coeff
|
||||||
|
}
|
||||||
|
|
||||||
|
// urgencyPriority calculates urgency contribution from priority
|
||||||
|
// Order: High > Medium > Default > Low
|
||||||
|
func (t *Task) urgencyPriority(coeffs *UrgencyCoefficients) float64 {
|
||||||
|
switch t.Priority {
|
||||||
|
case PriorityHigh:
|
||||||
|
return coeffs.PriorityH
|
||||||
|
case PriorityMedium:
|
||||||
|
return coeffs.PriorityM
|
||||||
|
case PriorityDefault:
|
||||||
|
return coeffs.PriorityD
|
||||||
|
case PriorityLow:
|
||||||
|
return coeffs.PriorityL
|
||||||
|
default:
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// urgencyAge calculates urgency contribution from task age
|
||||||
|
// Age increases linearly up to maxDays, then caps
|
||||||
|
func (t *Task) urgencyAge(coeff float64, maxDays int) float64 {
|
||||||
|
now := timeNow()
|
||||||
|
ageDuration := now.Sub(t.Created)
|
||||||
|
ageDays := int(ageDuration.Hours() / 24.0)
|
||||||
|
|
||||||
|
if ageDays > maxDays {
|
||||||
|
ageDays = maxDays
|
||||||
|
}
|
||||||
|
|
||||||
|
ageRatio := float64(ageDays) / float64(maxDays)
|
||||||
|
return ageRatio * coeff
|
||||||
|
}
|
||||||
|
|
||||||
|
// urgencyActive calculates urgency contribution for started tasks
|
||||||
|
func (t *Task) urgencyActive(coeff float64) float64 {
|
||||||
|
if t.Start != nil {
|
||||||
|
return coeff
|
||||||
|
}
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// urgencyWaiting calculates urgency penalty for waiting tasks
|
||||||
|
func (t *Task) urgencyWaiting(coeff float64) float64 {
|
||||||
|
if t.Wait != nil {
|
||||||
|
now := timeNow()
|
||||||
|
if t.Wait.After(now) {
|
||||||
|
return coeff // Should be negative coefficient
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// urgencyTags calculates urgency contribution from tags
|
||||||
|
// Special urgent tag (configurable, default "next") has high coefficient
|
||||||
|
// Regular tags contribute with diminishing returns (0.8 for 1, 0.9 for 2, 1.0 for 3+)
|
||||||
|
func (t *Task) urgencyTags(coeffs *UrgencyCoefficients) float64 {
|
||||||
|
tagCount := len(t.Tags)
|
||||||
|
if tagCount == 0 {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for special urgent tag first
|
||||||
|
for _, tag := range t.Tags {
|
||||||
|
if tag == coeffs.UrgentTag {
|
||||||
|
return coeffs.UrgentCoeff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply general tag coefficient with multiplier based on count
|
||||||
|
var multiplier float64
|
||||||
|
switch tagCount {
|
||||||
|
case 1:
|
||||||
|
multiplier = 0.8
|
||||||
|
case 2:
|
||||||
|
multiplier = 0.9
|
||||||
|
default:
|
||||||
|
multiplier = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
return multiplier * coeffs.Tags
|
||||||
|
}
|
||||||
|
|
||||||
|
// urgencyProject calculates urgency contribution from having a project
|
||||||
|
func (t *Task) urgencyProject(coeff float64) float64 {
|
||||||
|
if t.Project != nil {
|
||||||
|
return coeff
|
||||||
|
}
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildUrgencyCoefficients creates UrgencyCoefficients from config
|
||||||
|
func BuildUrgencyCoefficients(cfg *Config) *UrgencyCoefficients {
|
||||||
|
return &UrgencyCoefficients{
|
||||||
|
Due: cfg.UrgencyDue,
|
||||||
|
PriorityH: cfg.UrgencyPriorityH,
|
||||||
|
PriorityM: cfg.UrgencyPriorityM,
|
||||||
|
PriorityD: cfg.UrgencyPriorityD,
|
||||||
|
PriorityL: cfg.UrgencyPriorityL,
|
||||||
|
Active: cfg.UrgencyActive,
|
||||||
|
Age: cfg.UrgencyAge,
|
||||||
|
AgeMax: cfg.UrgencyAgeMax,
|
||||||
|
Tags: cfg.UrgencyTags,
|
||||||
|
Project: cfg.UrgencyProject,
|
||||||
|
Waiting: cfg.UrgencyWaiting,
|
||||||
|
UrgentTag: cfg.UrgencyUrgentTag,
|
||||||
|
UrgentCoeff: cfg.UrgencyUrgentCoeff,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user