// ============================================================================ // todoremind.go — 待办提醒调度 // // 定时扫描待办文件中的 `@ 时间` 标记,到点广播 todoReminder 事件 // 并唤出主窗口弹出置顶提醒。 // ============================================================================ package main import ( "os" "regexp" "strings" "sync" "time" ) // 待办提醒标记格式:任务文本 @ YYYY-MM-DD HH:MM 或 任务文本 @ HH:MM(当天) // 解析出的提醒时间存于 todoReminderItem。 type todoReminderItem struct { Text string // 任务文本(不含 @ 标记) When time.Time // 提醒时刻 RawLine string // 原始行(用于去重,避免同一行重复提醒) Fired bool // 是否已触发过 } // 匹配行尾的 @ 时间标记:@ 2026-10-31 17:30 或 @ 17:30(支持 [x] 前缀) var todoReminderRe = regexp.MustCompile(`(?i)^(\[x\]\s*)?(.*?)\s*@\s*((\d{4}-\d{2}-\d{2}\s+)?\d{1,2}:\d{2})\s*$`) // todoReminderState 提醒调度器状态:扫描间隔、已触发去重、上次扫描。 var ( todoReminderMu sync.Mutex todoReminderFired = map[string]bool{} // 已触发过的原始行,避免重复提醒 ) // parseTodoReminder 解析一行待办,提取任务文本与提醒时间。 // 无 @ 标记或格式不合法时返回 nil。 func parseTodoReminder(line string) *todoReminderItem { m := todoReminderRe.FindStringSubmatch(line) if m == nil { return nil } done := strings.TrimSpace(m[1]) != "" text := strings.TrimSpace(m[2]) timeStr := m[3] dateStr := strings.TrimSpace(m[4]) if text == "" || timeStr == "" { return nil } // 解析时间:有日期用日期+时间,否则用今天 layout := "15:04" if dateStr != "" { layout = "2006-01-02 15:04" } t, err := time.ParseInLocation(layout, timeStr, time.Local) if err != nil { return nil } // 无日期:默认当天;若时刻已过,则不再提醒(视为过期) if dateStr == "" { t = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), t.Hour(), t.Minute(), 0, 0, time.Local) } _ = done // 提醒触发条件只针对未完成任务(在前端判断),后端不据此跳过 return &todoReminderItem{Text: text, When: t, RawLine: strings.TrimSpace(line)} } // startTodoReminderLoop 每 15 秒扫描待办文件,把到点的提醒通过事件广播给前端。 // 只在主窗口存在时广播;已触发过的行不再重复提醒。 func startTodoReminderLoop() { for { scanTodoReminders() time.Sleep(15 * time.Second) } } // scanTodoReminders 读取待办文件,检查是否有到点的提醒,广播触发。 func scanTodoReminders() { if app == nil || mainWindow == nil { return } path := loadSettings().TodoPath if path == "" { return } data, err := os.ReadFile(path) if err != nil { return // 文件不存在或读取失败:忽略 } now := time.Now() todoReminderMu.Lock() defer todoReminderMu.Unlock() for _, line := range strings.Split(string(data), "\n") { item := parseTodoReminder(strings.TrimRight(line, "\r")) if item == nil { continue } // 只提醒未到点的未来时间;已触发过的不再提醒 if item.When.Before(now) { continue } if item.When.After(now.Add(15 * time.Second)) { continue // 还没到点 } if todoReminderFired[item.RawLine] { continue // 已提醒过 } todoReminderFired[item.RawLine] = true // 广播提醒:任务文本 + 提醒时刻 payload := item.Text + "\n" + item.When.Format("2006-01-02 15:04") app.Event.Emit("todoReminder", payload) // 弹出提醒时唤出主窗口(若隐藏) showMainWindow() } }