flake-update-20260505
  1// Package org provides org-mode integration for scheduling TODOs.
  2package org
  3
  4import (
  5	"bufio"
  6	"bytes"
  7	"fmt"
  8	"os"
  9	"os/exec"
 10	"strings"
 11	"time"
 12)
 13
 14// ScheduledItem represents an org TODO scheduled for a specific day.
 15type ScheduledItem struct {
 16	Heading string
 17	State   string // TODO, DONE, STRT, etc.
 18}
 19
 20// TodayItems returns org items scheduled for today from the given file.
 21func TodayItems(orgFile string) ([]ScheduledItem, error) {
 22	today := time.Now().Format("2006-01-02")
 23	return itemsForDate(orgFile, today)
 24}
 25
 26func itemsForDate(orgFile, date string) ([]ScheduledItem, error) {
 27	f, err := os.Open(orgFile)
 28	if err != nil {
 29		return nil, err
 30	}
 31	defer f.Close()
 32
 33	var items []ScheduledItem
 34	scanner := bufio.NewScanner(f)
 35	var prevLine string
 36	for scanner.Scan() {
 37		line := scanner.Text()
 38		if strings.Contains(line, fmt.Sprintf("SCHEDULED: <%s", date)) ||
 39			strings.Contains(line, fmt.Sprintf("DEADLINE: <%s", date)) {
 40			// Previous line should be the heading
 41			if strings.HasPrefix(strings.TrimSpace(prevLine), "**") {
 42				heading := strings.TrimSpace(prevLine)
 43				// Strip leading stars
 44				for strings.HasPrefix(heading, "*") {
 45					heading = strings.TrimPrefix(heading, "*")
 46				}
 47				heading = strings.TrimSpace(heading)
 48
 49				state := ""
 50				for _, s := range []string{"TODO", "DONE", "NEXT", "STRT", "WAIT", "CANX"} {
 51					if strings.HasPrefix(heading, s+" ") {
 52						state = s
 53						heading = strings.TrimPrefix(heading, s+" ")
 54						break
 55					}
 56				}
 57				items = append(items, ScheduledItem{
 58					Heading: strings.TrimSpace(heading),
 59					State:   state,
 60				})
 61			}
 62		}
 63		prevLine = line
 64	}
 65	return items, scanner.Err()
 66}
 67
 68// HasJiraKey checks if a Jira issue key already exists in the org file (as a non-DONE TODO).
 69func HasJiraKey(orgFile, key string) bool {
 70	return hasProperty(orgFile, "JIRA_KEY", key)
 71}
 72
 73// HasGitHubIssue checks if a GitHub issue already exists in the org file (as a non-DONE TODO).
 74func HasGitHubIssue(orgFile, repo string, number int) bool {
 75	return hasPropertyPair(orgFile, "GH_REPO", repo, "GH_NUMBER", fmt.Sprintf("%d", number))
 76}
 77
 78func hasProperty(orgFile, propName, propValue string) bool {
 79	f, err := os.Open(orgFile)
 80	if err != nil {
 81		return false
 82	}
 83	defer f.Close()
 84
 85	needle := fmt.Sprintf(":%s: %s", propName, propValue)
 86	scanner := bufio.NewScanner(f)
 87	for scanner.Scan() {
 88		if strings.Contains(scanner.Text(), needle) {
 89			return true
 90		}
 91	}
 92	return false
 93}
 94
 95func hasPropertyPair(orgFile, prop1, val1, prop2, val2 string) bool {
 96	f, err := os.Open(orgFile)
 97	if err != nil {
 98		return false
 99	}
100	defer f.Close()
101
102	needle1 := fmt.Sprintf(":%s: %s", prop1, val1)
103	needle2 := fmt.Sprintf(":%s: %s", prop2, val2)
104	found1 := false
105	scanner := bufio.NewScanner(f)
106	for scanner.Scan() {
107		line := scanner.Text()
108		if strings.Contains(line, needle1) {
109			found1 = true
110		}
111		if found1 && strings.Contains(line, needle2) {
112			return true
113		}
114		// Reset if we leave a :PROPERTIES: block
115		if found1 && strings.TrimSpace(line) == ":END:" {
116			found1 = false
117		}
118	}
119	return false
120}
121
122// ScheduleJiraIssue creates an org TODO for a Jira issue under the given section.
123func ScheduleJiraIssue(orgFile, section, key, summary, url string, date time.Time) error {
124	dateStr := date.Format("2006-01-02 Mon")
125	createdStr := time.Now().Format("2006-01-02 Mon 15:04")
126
127	orgContent := fmt.Sprintf(
128		"\n** TODO [[%s][%s]] %s  :jira:\nSCHEDULED: <%s>\n:PROPERTIES:\n:JIRA_KEY: %s\n:URL: %s\n:CREATED: [%s]\n:END:\n",
129		url, key, summary, dateStr, key, url, createdStr,
130	)
131
132	return appendToSection(orgFile, section, orgContent)
133}
134
135// ScheduleGitHubIssue creates an org TODO for a GitHub issue under the given section.
136func ScheduleGitHubIssue(orgFile, section, repo string, number int, summary, url string, date time.Time) error {
137	dateStr := date.Format("2006-01-02 Mon")
138	createdStr := time.Now().Format("2006-01-02 Mon 15:04")
139	ref := fmt.Sprintf("%s#%d", repo, number)
140
141	orgContent := fmt.Sprintf(
142		"\n** TODO [[%s][%s]] %s  :github:\nSCHEDULED: <%s>\n:PROPERTIES:\n:GH_REPO: %s\n:GH_NUMBER: %d\n:URL: %s\n:CREATED: [%s]\n:END:\n",
143		url, ref, summary, dateStr, repo, number, url, createdStr,
144	)
145
146	return appendToSection(orgFile, section, orgContent)
147}
148
149func appendToSection(orgFile, section, content string) error {
150	escapedSection := strings.ReplaceAll(section, `"`, `\"`)
151	escapedContent := strings.ReplaceAll(content, `"`, `\"`)
152	escapedContent = strings.ReplaceAll(escapedContent, `\`, `\\`)
153
154	elisp := fmt.Sprintf(`
155      (with-current-buffer (find-file-noselect %q)
156        (goto-char (point-min))
157        (re-search-forward "^\\* %s")
158        (org-end-of-subtree t)
159        (insert %q)
160        (save-buffer)
161        "ok")`, orgFile, escapedSection, content)
162
163	cmd := exec.Command("emacsclient", "--eval", elisp)
164	var stderr bytes.Buffer
165	cmd.Stderr = &stderr
166	if err := cmd.Run(); err != nil {
167		return fmt.Errorf("emacsclient error: %s: %w", stderr.String(), err)
168	}
169	return nil
170}