Commit 4bb4441afd8a
Changed files (4)
tools
daily-plan
tools/daily-plan/cmd/daily-plan/main.go
@@ -104,14 +104,15 @@ func run(args []string) error {
// JSON output types for Emacs integration.
type jsonShow struct {
- Date string `json:"date"`
- Agenda []jsonOrg `json:"agenda"`
- JiraInProgress []jsonJira `json:"jira_in_progress"`
- JiraBacklog []jsonJira `json:"jira_backlog"`
- GHIssues []jsonGH `json:"github_issues"`
- GHAssignedPRs []jsonGH `json:"github_assigned_prs"`
- GHReviews []jsonGH `json:"github_reviews"`
- GHPRs []jsonGH `json:"github_prs"`
+ Date string `json:"date"`
+ Agenda []jsonOrg `json:"agenda"`
+ Deadlines []jsonDeadline `json:"deadlines"`
+ JiraInProgress []jsonJira `json:"jira_in_progress"`
+ JiraBacklog []jsonJira `json:"jira_backlog"`
+ GHIssues []jsonGH `json:"github_issues"`
+ GHAssignedPRs []jsonGH `json:"github_assigned_prs"`
+ GHReviews []jsonGH `json:"github_reviews"`
+ GHPRs []jsonGH `json:"github_prs"`
}
type jsonInbox struct {
@@ -152,6 +153,13 @@ type jsonOrg struct {
Heading string `json:"heading"`
}
+type jsonDeadline struct {
+ State string `json:"state"`
+ Heading string `json:"heading"`
+ Deadline string `json:"deadline"`
+ Scheduled bool `json:"scheduled"`
+}
+
type jsonJira struct {
Key string `json:"key"`
Summary string `json:"summary"`
@@ -259,10 +267,23 @@ func emitJSON(v any) error {
return enc.Encode(v)
}
+// filterUnscheduledDeadlines keeps deadline items that are not already
+// scheduled (so they surface as things that might slip).
+func filterUnscheduledDeadlines(items []org.DeadlineItem) []org.DeadlineItem {
+ out := make([]org.DeadlineItem, 0, len(items))
+ for _, d := range items {
+ if !d.Scheduled {
+ out = append(out, d)
+ }
+ }
+ return out
+}
+
func cmdShow(ctx context.Context, cfg *config.Config) error {
today := time.Now().Format("2006-01-02")
agenda, _ := org.TodayItems(cfg.Org.File)
+ deadlines, _ := org.UpcomingDeadlines(cfg.Org.File, 7)
inprog, _ := cache.GetOrFetch(apiCache, "show:inprog:"+today, func() ([]jira.Issue, error) {
return jira.FetchByStatus(ctx, cfg.Jira.User, []string{"In Progress", "Code Review", "On QA"})
})
@@ -291,9 +312,14 @@ func cmdShow(ctx context.Context, cfg *config.Config) error {
for _, a := range agenda {
orgItems = append(orgItems, jsonOrg{State: a.State, Heading: a.Heading})
}
+ dl := make([]jsonDeadline, 0, len(deadlines))
+ for _, d := range deadlines {
+ dl = append(dl, jsonDeadline{State: d.State, Heading: d.Heading, Deadline: d.Deadline, Scheduled: d.Scheduled})
+ }
return emitJSON(jsonShow{
Date: today,
Agenda: orgItems,
+ Deadlines: dl,
JiraInProgress: jiraToJSON(inprog, cfg.Jira.BaseURL),
JiraBacklog: jiraToJSON(todo, cfg.Jira.BaseURL),
GHIssues: ghToJSON(ghIssues),
@@ -306,6 +332,11 @@ func cmdShow(ctx context.Context, cfg *config.Config) error {
display.Header(fmt.Sprintf("Today's Org Agenda (%s)", today))
display.OrgItems(agenda)
+ if unscheduled := filterUnscheduledDeadlines(deadlines); len(unscheduled) > 0 {
+ display.Header("Upcoming Deadlines (unscheduled, next 7 days)")
+ display.DeadlineItems(unscheduled)
+ }
+
display.Header("Jira — In Progress / Code Review")
display.JiraIssues(inprog, "active")
tools/daily-plan/internal/display/display.go
@@ -48,6 +48,17 @@ func OrgItems(items []org.ScheduledItem) {
}
}
+// DeadlineItems prints org items with an upcoming deadline.
+func DeadlineItems(items []org.DeadlineItem) {
+ for _, item := range items {
+ state := item.State
+ if state != "" {
+ state += " "
+ }
+ fmt.Printf(" %s⚑ %s%s %s%s%s\n", red, item.Deadline, reset, state, item.Heading, reset)
+ }
+}
+
// JiraIssues prints a list of Jira issues.
func JiraIssues(issues []jira.Issue, style string) {
if len(issues) == 0 {
tools/daily-plan/internal/org/org.go
@@ -7,6 +7,7 @@ import (
"fmt"
"os"
"os/exec"
+ "regexp"
"strings"
"time"
)
@@ -23,6 +24,80 @@ func TodayItems(orgFile string) ([]ScheduledItem, error) {
return itemsForDate(orgFile, today)
}
+// DeadlineItem represents an org TODO with a DEADLINE in a date range.
+type DeadlineItem struct {
+ Heading string
+ State string // TODO, NEXT, STRT, WAIT
+ Deadline string // YYYY-MM-DD
+ Scheduled bool // whether the item is also SCHEDULED
+ Date time.Time // parsed deadline, for sorting
+}
+
+var deadlineRe = regexp.MustCompile(`DEADLINE:\s+<(\d{4}-\d{2}-\d{2})`)
+
+// UpcomingDeadlines returns non-DONE org items whose DEADLINE falls within
+// [today, today+days]. The Scheduled flag reports whether the same item also
+// carries a SCHEDULED stamp (org emits planning lines on the line following the
+// heading, scheduled/deadline possibly combined on one line).
+func UpcomingDeadlines(orgFile string, days int) ([]DeadlineItem, error) {
+ f, err := os.Open(orgFile)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ now := time.Now()
+ today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
+ until := today.AddDate(0, 0, days)
+
+ var items []DeadlineItem
+ scanner := bufio.NewScanner(f)
+ var prevLine string
+ for scanner.Scan() {
+ line := scanner.Text()
+ m := deadlineRe.FindStringSubmatch(line)
+ if m == nil {
+ prevLine = line
+ continue
+ }
+ d, perr := time.ParseInLocation("2006-01-02", m[1], now.Location())
+ if perr != nil || d.Before(today) || d.After(until) {
+ prevLine = line
+ continue
+ }
+ heading := strings.TrimSpace(prevLine)
+ if !strings.HasPrefix(heading, "*") {
+ prevLine = line
+ continue
+ }
+ for strings.HasPrefix(heading, "*") {
+ heading = strings.TrimPrefix(heading, "*")
+ }
+ heading = strings.TrimSpace(heading)
+ state := ""
+ for _, s := range []string{"TODO", "DONE", "NEXT", "STRT", "WAIT", "CANX"} {
+ if strings.HasPrefix(heading, s+" ") {
+ state = s
+ heading = strings.TrimPrefix(heading, s+" ")
+ break
+ }
+ }
+ if state == "DONE" || state == "CANX" {
+ prevLine = line
+ continue
+ }
+ items = append(items, DeadlineItem{
+ Heading: strings.TrimSpace(heading),
+ State: state,
+ Deadline: m[1],
+ Scheduled: strings.Contains(line, "SCHEDULED:"),
+ Date: d,
+ })
+ prevLine = line
+ }
+ return items, scanner.Err()
+}
+
func itemsForDate(orgFile, date string) ([]ScheduledItem, error) {
f, err := os.Open(orgFile)
if err != nil {
tools/daily-plan/daily-plan.el
@@ -150,6 +150,11 @@
(insert (format " =[%s]=" .project)))
(insert (format " /(%s)/\n" .date))))
+(defun daily-plan--insert-deadline-item (item)
+ "Insert an org ITEM with an upcoming DEADLINE."
+ (let-alist item
+ (insert (format "- <%s> %s %s\n" .deadline (or .state "") .heading))))
+
;; ── Render: show ──
(defun daily-plan--render-show (data)
@@ -164,6 +169,15 @@
(insert (format "- %s %s\n" (or .state "") .heading))))
(insert "- (nothing scheduled)\n"))
+ ;; Upcoming deadlines that are not yet scheduled (candidates to slip).
+ (let ((unscheduled (seq-remove
+ (lambda (it) (eq t (alist-get 'scheduled it)))
+ .deadlines)))
+ (when unscheduled
+ (insert "\n** Upcoming Deadlines — unscheduled (next 7 days) :deadline:\n")
+ (dolist (item unscheduled)
+ (daily-plan--insert-deadline-item item))))
+
(insert "\n** Jira — In Progress / Code Review\n")
(daily-plan--insert-items .jira_in_progress #'daily-plan--insert-jira-item nil)