92std::expected<SyncResult, SyncError> sync_task(
const TaskReminder& task, Provider provider,
93 const SyncOptions& options = {});
122 return !value.empty() && std::ranges::all_of(value, [](
char character) {
123 return std::isalnum(
static_cast<unsigned char>(character)) || character ==
'-' ||
129std::optional<std::array<int, 3>>
split_date(std::string_view value) {
130 if (value.size() != 10 || value[4] !=
'-' || value[7] !=
'-') {
134 const int year = std::stoi(std::string(value.substr(0, 4)));
135 const int month = std::stoi(std::string(value.substr(5, 2)));
136 const int day = std::stoi(std::string(value.substr(8, 2)));
137 const std::chrono::year_month_day date{
138 std::chrono::year{year},
139 std::chrono::month{
static_cast<unsigned>(month)},
140 std::chrono::day{
static_cast<unsigned>(day)},
145 return std::array{year, month, day};
146 }
catch (
const std::exception&) {
157 std::array<char, 4096> buffer{};
159 const ssize_t count = ::read(descriptor, buffer.data(), buffer.size());
161 result.append(buffer.data(),
static_cast<std::size_t
>(count));
162 }
else if (count == 0) {
164 }
else if (errno != EINTR) {
175std::expected<ProcessResult, SyncError>
run_osascript(std::string_view script,
176 const std::vector<std::string>& arguments) {
178 if (!std::filesystem::exists(
"/usr/bin/osascript")) {
182 int stdout_pipe[2]{};
183 int stderr_pipe[2]{};
185 if (::pipe(stdout_pipe) != 0 || ::pipe(stderr_pipe) != 0) {
189 posix_spawn_file_actions_t actions;
190 posix_spawn_file_actions_init(&actions);
193 posix_spawn_file_actions_adddup2(&actions, stdout_pipe[1], STDOUT_FILENO);
194 posix_spawn_file_actions_adddup2(&actions, stderr_pipe[1], STDERR_FILENO);
195 posix_spawn_file_actions_addclose(&actions, stdout_pipe[0]);
196 posix_spawn_file_actions_addclose(&actions, stderr_pipe[0]);
197 posix_spawn_file_actions_addclose(&actions, stdout_pipe[1]);
198 posix_spawn_file_actions_addclose(&actions, stderr_pipe[1]);
200 std::vector<std::string> owned_arguments;
201 owned_arguments.reserve(arguments.size() + 3);
204 owned_arguments.emplace_back(
"/usr/bin/osascript");
205 owned_arguments.emplace_back(
"-e");
206 owned_arguments.emplace_back(script);
207 owned_arguments.emplace_back(
"--");
208 owned_arguments.insert(owned_arguments.end(), arguments.begin(), arguments.end());
209 std::vector<char*> argv;
210 argv.reserve(owned_arguments.size() + 1);
211 for (
auto& argument : owned_arguments) {
212 argv.push_back(argument.data());
214 argv.push_back(
nullptr);
218 const int spawn_result =
219 ::posix_spawn(&child,
"/usr/bin/osascript", &actions,
nullptr, argv.data(),
environ);
220 posix_spawn_file_actions_destroy(&actions);
223 ::close(stdout_pipe[1]);
224 ::close(stderr_pipe[1]);
225 if (spawn_result != 0) {
226 ::close(stdout_pipe[0]);
227 ::close(stderr_pipe[0]);
235 ::close(stdout_pipe[0]);
236 ::close(stderr_pipe[0]);
240 while (::waitpid(child, &status, 0) < 0 && errno == EINTR) {
242 if (WIFEXITED(status)) {
244 }
else if (WIFSIGNALED(status)) {
245 result.
exit_code = 128 + WTERMSIG(status);
260 std::to_string((*date)[0]),
261 std::to_string((*date)[1]),
262 std::to_string((*date)[2]),
263 task.
done ?
"true" :
"false",
271 return R
"APPLESCRIPT(
273 set taskName to item 1 of argv
274 set taskBody to item 2 of argv
275 set marker to item 3 of argv
276 set dueYearText to item 4 of argv
277 set dueMonthText to item 5 of argv
278 set dueDayText to item 6 of argv
279 set completedValue to (item 7 of argv) is "true"
280 set listName to item 8 of argv
282 set dueDate to date (dueYearText & "-" & dueMonthText & "-" & dueDayText)
283 set time of dueDate to 86340
285 tell application "Reminders"
287 set targetList to missing value
288 repeat with candidate in lists
289 if name of candidate is listName then
290 set targetList to candidate
294 if targetList is missing value then
295 set targetList to make new list with properties {name:listName}
298 -- 隐藏标记承担 upsert 主键;重复同步只更新,不新增副本。
299 set matches to every reminder of targetList whose body contains marker
300 if (count of matches) > 0 then
301 set targetReminder to item 1 of matches
302 set name of targetReminder to taskName
303 set body of targetReminder to taskBody
304 set due date of targetReminder to dueDate
306 set targetReminder to make new reminder at end of targetList with properties {name:taskName, body:taskBody, due date:dueDate}
308 set completed of targetReminder to completedValue
309 return "Reminders:" & (id of targetReminder)
315std::string build_omnifocus_script() {
317 return R
"APPLESCRIPT(
319 set taskName to item 1 of argv
320 set taskBody to item 2 of argv
321 set marker to item 3 of argv
322 set dueYearText to item 4 of argv
323 set dueMonthText to item 5 of argv
324 set dueDayText to item 6 of argv
325 set completedValue to (item 7 of argv) is "true"
326 set dueDate to date (dueYearText & "-" & dueMonthText & "-" & dueDayText)
327 set time of dueDate to 86340
329 tell application "OmniFocus"
330 set targetDocument to default document
332 -- 只更新 Inbox/项目中的现有任务;首次同步创建 Inbox task。
333 set targetTask to missing value
334 repeat with candidate in flattened tasks
335 if (note of candidate contains marker) then
336 set targetTask to candidate
340 if targetTask is missing value then
341 set targetTask to make new inbox task with properties {name:taskName, note:taskBody, due date:dueDate}
343 set name of targetTask to taskName
344 set note of targetTask to taskBody
345 set due date of targetTask to dueDate
347 if completedValue then
348 mark complete targetTask
350 mark incomplete targetTask
352 return "OmniFocus:" & (id of targetTask)
362 const std::string input =
363 std::format(
"task_id={} title={} due={} provider={} dry_run={}", task.
task_id, task.
title,
365 const auto operation = [&]() -> std::expected<SyncResult, SyncError> {
367 if (!valid_task_id(task.
task_id)) {
374 return std::unexpected(SyncError::invalid_due_date);
380 const std::string body = std::format(
"{}\n\n{}", task.
description, result.marker);
388 result.reminders_ok = use_reminders;
389 result.omnifocus_ok = use_omnifocus;
390 result.reminders_output = use_reminders ?
"dry-run: reminders script ready" :
"";
391 result.omnifocus_output = use_omnifocus ?
"dry-run: OmniFocus script ready" :
"";
403 if (!process || process->exit_code != 0) {
406 result.reminders_ok =
true;
407 result.reminders_output = process->output;
418 if (!process || process->exit_code != 0) {
421 result.omnifocus_ok =
true;
422 result.omnifocus_output = process->output;
428 const bool ok = operation.has_value();
430 interface_log::ApiName(
"ReminderSync::sync_task"), interface_log::LogText(input),
431 interface_log::LogText(ok ? std::format(
"reminders={} omnifocus={}", operation->reminders_ok,
432 operation->omnifocus_ok)
455 return "task id is invalid";
457 return "task has no due date";
459 return "task due date is invalid";
461 return "osascript is unavailable";
463 return "Apple Reminders sync failed";
465 return "OmniFocus sync failed";
467 return "failed to launch osascript";
469 return "unknown reminder sync error";
将带截止日期的 Stacks 任务同步到 Apple Reminders / OmniFocus。
std::string read_descriptor(int descriptor)
读到 EOF 的管道读取器。
std::expected< ProcessResult, SyncError > run_osascript(std::string_view script, const std::vector< std::string > &arguments)
启动 osascript,并通过独立 argv 传递用户字段。
bool valid_task_id(std::string_view value)
任务 ID 只允许字母、数字、-、_,防止注入 action/AppleScript 参数。
std::optional< std::array< int, 3 > > split_date(std::string_view value)
严格拆分并校验 YYYY-MM-DD;用于 AppleScript 日期构造前的领域校验。
std::vector< std::string > common_arguments(const TaskReminder &task, std::string_view marker)
构造两个 AppleScript 共用的位置参数序列。
@ process_failure
管道或进程创建失败。
@ osascript_unavailable
系统缺少 osascript。
@ due_date_required
缺少截止日期。
@ invalid_due_date
日期格式或日历值非法。
@ reminders_failed
Reminders 脚本非零退出。
@ invalid_task_id
任务 ID 含 token 不安全字符。
@ omnifocus_failed
OmniFocus 脚本非零退出。
Provider
提醒事项提供方;both 表示两个目标都执行。
@ reminders
Apple Reminders。
@ both
Reminders + OmniFocus。
std::string provider_name(Provider provider) noexcept
Provider 到 CLI/日志稳定名称。
std::string build_reminders_script()
返回可在 osacompile 中独立编译的 Reminders AppleScript 源。
std::expected< SyncResult, SyncError > sync_task(const TaskReminder &task, Provider provider, const SyncOptions &options={})
将一个任务同步到指定外部应用。
std::string build_omnifocus_script()
返回可在 osacompile 中独立编译的 OmniFocus AppleScript 源。
std::string_view sync_error_message(SyncError error) noexcept
SyncError 到稳定可读文本。
std::vector< std::string > arguments(int argc, char *argv[], int start)
从 argv[start..] 复制额外参数;start 之后的参数所有权仍属于进程。
void record(ApiName api, LogText input, LogText output, ApiStatus status)
记录一次接口调用。
@ failed
调用失败(错误已记录在 output 文本中)
std::string omnifocus_bundle_id
OmniFocus 应用 ID。
std::string reminders_list
Reminders 目标列表。
bool dry_run
只验证,不调用 osascript。
同步结果;每个后端独立报告成败,便于 CLI 精确展示。
std::string marker
[Stacks-ID:<id>] 去重标记。
std::string omnifocus_output
OmniFocus 脚本输出。
std::string reminders_output
Reminders 脚本输出。
bool omnifocus_ok
OmniFocus 是否完成。
bool reminders_ok
Reminders 是否完成。
std::string task_id
用于去重标记和格式校验。
std::string description
提醒正文;同步时追加去重标记。
std::string due_date
YYYY-MM-DD。
int exit_code
正常退出 0;信号退出为 128 + signal。
std::string output
stdout。