891 const std::string query = trim(std::string(raw_query));
892 json items = json::array();
895 const CommandAliases aliases;
899 const json help_item = item(
902 std::format(
"{} 添加路径 · {} 重载 · {} 查看 · {} 搜索 · {} 本周 · {} 计时 · {} "
904 aliases.add_path, aliases.reload, aliases.view, aliases.tasks,
905 aliases.week, aliases.timers, aliases.inbox, aliases.config),
906 std::format(
"{} add path · {} reload · {} view · {} search · {} week · {} timers · "
907 "{} inbox · {} config",
908 aliases.add_path, aliases.reload, aliases.view, aliases.tasks,
909 aliases.week, aliases.timers, aliases.inbox, aliases.config)),
910 "",
"",
false, icon_note());
914 std::format(
"add-path · {} — 选择新目录并加入索引", aliases.add_path),
915 std::format(
"add-path · {} — choose and index a new library", aliases.add_path)),
916 "", aliases.add_path +
" ",
false, icon_path(context)));
919 bilingual(std::format(
"reload · {} — 重新扫描所有已保存路径", aliases.reload),
920 std::format(
"reload · {} — rescan all saved libraries", aliases.reload)),
921 "", aliases.reload,
false, icon_path(context)));
924 bilingual(std::format(
"view · {} — 项目级 → 清单级 → 任务级", aliases.view),
925 std::format(
"view · {} — project → list → task", aliases.view)),
926 "", aliases.view +
" ",
false, icon_path(context)));
927 items.push_back(item(
930 std::format(
"tasks · {} — 搜索全部工作区、项目和清单", aliases.tasks),
931 std::format(
"tasks · {} — search every library, project and list", aliases.tasks)),
932 "", aliases.tasks +
" ",
false, icon_path(context)));
933 items.push_back(item(
935 bilingual(std::format(
"week · {} — 聚合全部路径", aliases.week),
936 std::format(
"week · {} — due this week across all libraries", aliases.week)),
937 "", aliases.week,
false, icon_path(context)));
938 items.push_back(item(
940 bilingual(std::format(
"timers · {} — 查看或停止计时", aliases.timers),
941 std::format(
"timers · {} — inspect or stop running timers", aliases.timers)),
942 "", aliases.timers,
false, icon_path(context)));
943 items.push_back(item(
945 bilingual(std::format(
"inbox · {} — 先选择路径,默认第一个", aliases.inbox),
946 std::format(
"inbox · {} — choose a library; first is default", aliases.inbox)),
947 "", aliases.inbox +
" ",
false, icon_path(context)));
950 bilingual(std::format(
"config · {} — 查看已保存路径", aliases.config),
951 std::format(
"config · {} — inspect saved library paths", aliases.config)),
952 "", aliases.config,
false, icon_path(context)));
956 return feedback(items);
958 const json board_rows = board_items(*boards,
"",
true);
959 for (
const auto& board : board_rows) {
960 items.push_back(board);
962 items.push_back(help_item);
963 return feedback(items);
967 if (query ==
"reload-all") {
968 return feedback(json::array({item(
"重载全部路径",
969 bilingual(
"reload · rl — 重新扫描所有已保存路径",
970 "reload · rl — rescan all saved libraries"),
971 "reload-all",
"",
true, icon_path(context))}));
974 if (query ==
"add-path") {
975 return feedback(json::array({item(
"添加新路径",
976 bilingual(
"add-path · ap — 选择目录并加入索引",
977 "add-path · ap — choose a folder and add it"),
978 "add-path",
"",
true, icon_path(context))}));
981 if (query ==
"config") {
982 return feedback(config_items(context, aliases));
986 if (query ==
"add" || query ==
"board" || query.starts_with(
"board ")) {
989 return feedback(json::array(
993 if (query ==
"board" || query.starts_with(
"board ")) {
995 "查看与编辑 / View and edit",
997 "输入关键词模糊匹配项目或清单,回车进入下一级;任务详情内可返回上一级",
998 "Fuzzy-match projects or lists, press Enter to drill down, and use Back inside"),
999 "",
"",
false, icon_note());
1001 const std::string board_query = query.starts_with(
"board ") ? trim(query.substr(6)) :
"";
1002 const json board_rows = board_items(*boards, board_query, query ==
"add");
1003 for (
const auto& board : board_rows) {
1004 items.push_back(board);
1006 if (!help_item.is_null()) {
1007 items.push_back(help_item);
1009 return feedback(items);
1013 if (query.starts_with(
"add:")) {
1014 const auto [target, payload] = split_first(query.substr(4),
' ');
1017 return feedback(json::array(
1020 const auto board = std::ranges::find_if(
1022 if (board == boards->end()) {
1023 return feedback(json::array({error_item(
"找不到目标项目/清单", target)}));
1025 if (trim(payload).empty()) {
1026 return feedback(json::array(
1027 {item(std::format(
"{}:{}", board->kind ==
"project" ?
"项目" :
"清单", board->title),
1028 bilingual(
"继续输入:标题|描述|开始|截止|优先级",
1029 "Type title|description|start|due|priority"),
1030 "",
"",
false, board->kind ==
"project" ? icon_project() : icon_stack())}));
1032 const auto parsed = parse_task_fields(payload);
1033 if (parsed.title.empty()) {
1034 return feedback(json::array(
1035 {error_item(
"任务标题不能为空",
"Format: title|description|start|due|priority")}));
1038 return feedback(json::array({item(
1039 std::format(
"三级:任务|创建:{}", parsed.title),
1040 bilingual(std::format(
"项目:{} → 清单:{} · 开始 {} · 截止 {} · 优先级 {}",
1041 board->project_title, board->title,
1042 parsed.start_date.empty() ?
"未设置" : parsed.start_date,
1043 parsed.due_date.empty() ?
"未设置" : parsed.due_date,
1044 parsed.priority.empty() ?
"无" : priority_label(parsed.priority)),
1045 "Press Enter to create this task in the selected project/list"),
1046 std::format(
"create:{}:{}", target, encoded),
"",
true,
1047 board->kind ==
"project" ? icon_project() : icon_stack())}));
1051 if (query ==
"inbox") {
1052 const auto paths = library_paths(context);
1053 if (paths.empty()) {
1055 json::array({item(
"收件箱|没有已保存路径",
1056 bilingual(
"先执行 s ap 添加路径",
"Run s ap to add a library first"),
1057 "",
"",
false, icon_inbox())}));
1059 const json help_item =
1061 bilingual(
"先选择工作区;进入后直接查看、模糊搜索或新建收件箱任务",
1062 "Choose a library, then view, fuzzy-search or create Inbox tasks"),
1063 "",
"",
false, icon_note());
1064 for (std::size_t index = 0; index < paths.size(); ++index) {
1065 const std::string& path = paths[index];
1066 items.push_back(item(
1067 std::format(
"收件箱路径|{}{}", fs::path(path).filename().
string(),
1068 index == 0 ?
"(默认)" :
""),
1069 bilingual(std::format(
"{} · {}", path, index == 0 ?
"默认第一个路径" :
"已保存路径"),
1070 index == 0 ?
"Default (first) Inbox library" :
"Saved Inbox library"),
1071 "", std::format(
"inbox-workspace:{} ",
base64url_encode(path)),
false, icon_inbox()));
1073 items.push_back(help_item);
1074 return feedback(items);
1078 if (query.starts_with(
"inbox-workspace:")) {
1079 const auto [encoded_path, search] = split_first(query.substr(16),
' ');
1081 if (!path || path->empty()) {
1082 return feedback(json::array({error_item(
"收件箱路径无效",
"Invalid Inbox library path")}));
1085 const std::string search_text = trim(search);
1093 std::vector<TaskManager::TaskRecord> filtered;
1094 for (
const auto& task : *tasks) {
1095 if (task.library_path.lexically_normal() != fs::path(*path).lexically_normal()) {
1098 const std::string haystack = std::format(
"{} {}", task.title, task.description);
1099 if (fuzzy_match(haystack, search_text)) {
1100 filtered.push_back(task);
1103 const json help_item = item(
1104 std::format(
"收件箱工作区 / Inbox workspace:{}", fs::path(*path).filename().string()),
1105 bilingual(
"直接显示当前收件箱任务;输入关键词模糊匹配,无匹配时进入新建",
1106 "Inbox tasks are shown directly; fuzzy-match or create when nothing matches"),
1107 "",
"",
false, icon_note());
1109 if (!filtered.empty()) {
1110 const json task_rows = task_items(filtered);
1111 for (
const auto& task : task_rows) {
1112 items.push_back(task);
1114 }
else if (!search_text.empty()) {
1116 item(std::format(
"新建任务 / Create task:{}", search_text),
1117 bilingual(
"当前收件箱没有匹配任务,回车自动进入新建流程",
1118 "No matching Inbox task; press Enter to create it"),
1119 std::format(
"create-inbox:{}:{}", path_token,
base64url_encode(search_text)),
"",
1120 true, icon_task()));
1123 if (!filtered.empty()) {
1124 items.push_back(item(
1125 "新建收件箱任务 / Create another Inbox task",
1126 bilingual(
"保持当前任务列表,继续录入新任务",
"Keep the list and create another task"),
1127 "", std::format(
"inbox-create:{} ", path_token),
false, icon_task()));
1128 }
else if (search_text.empty()) {
1129 items.push_back(item(
"录入收件箱任务 / Create Inbox task",
1130 bilingual(
"当前工作区收件箱无任务时直接进入录入",
1131 "Create directly when this Inbox has no matching tasks"),
1132 "", std::format(
"inbox-create:{} ", path_token),
false, icon_task()));
1134 items.push_back(help_item);
1135 items.push_back(item(
"← 返回路径列表 / Back",
1136 bilingual(
"返回全部收件箱工作区",
"Back to all Inbox libraries"),
"",
1137 "inbox ",
false, icon_note()));
1138 return feedback(items);
1142 if (query.starts_with(
"inbox-create:")) {
1143 const auto [encoded_path, payload] = split_first(query.substr(13),
' ');
1145 if (!path || path->empty()) {
1146 return feedback(json::array({error_item(
"收件箱路径无效",
"Invalid Inbox library path")}));
1148 if (trim(payload).empty()) {
1149 return feedback(json::array({item(
"收件箱|填写任务",
1150 bilingual(
"继续输入:标题|描述|开始|截止|优先级",
1151 "Type title|description|start|due|priority"),
1152 "",
"",
false, icon_inbox())}));
1154 const auto parsed = parse_task_fields(payload);
1155 if (parsed.title.empty()) {
1157 json::array({error_item(
"任务标题不能为空",
"Task title cannot be empty")}));
1159 return feedback(json::array({item(
1160 std::format(
"三级:任务|添加到收件箱:{}", parsed.title),
1161 bilingual(std::format(
"路径:{} · 开始 {} · 截止 {} · 优先级 {}", *path,
1162 parsed.start_date.empty() ?
"未设置" : parsed.start_date,
1163 parsed.due_date.empty() ?
"未设置" : parsed.due_date,
1164 parsed.priority.empty() ?
"无" : priority_label(parsed.priority)),
1165 "Library, plan dates and priority are shown above; press Enter to create"),
1167 true, icon_inbox())}));
1171 if (query.starts_with(
"inbox ")) {
1173 json::array({item(
"s i 后不接受直接文本 / Text after s i is blocked",
1174 bilingual(
"请先选择工作区,再直接搜索或新建收件箱任务",
1175 "Select a library first, then search or create Inbox tasks"),
1176 "",
"inbox",
false, icon_inbox())}));
1180 if (query ==
"tasks") {
1186 json rows = task_items(*tasks);
1187 rows.push_back(item(
"全局任务搜索",
1188 bilingual(
"范围:全部工作区 → 全部项目 → 全部清单",
1189 "Scope: every library → every project → every list"),
1190 "",
"",
false, icon_note()));
1191 return feedback(rows);
1193 if (query.starts_with(
"tasks ")) {
1195 options.
query = trim(query.substr(6));
1201 json rows = task_items(*tasks);
1202 rows.push_back(item(std::format(
"全局任务搜索|{}", trim(query.substr(6))),
1203 bilingual(
"范围:全部工作区 → 全部项目 → 全部清单",
1204 "Scope: every library → every project → every list"),
1205 "",
"",
false, icon_note()));
1206 return feedback(rows);
1210 if (query.starts_with(
"board:")) {
1211 const auto [target, search] = split_first(query.substr(6),
' ');
1219 const json help_item =
1221 bilingual(
"输入关键词模糊匹配任务;回车编辑,匹配不到时进入插入流程",
1222 "Fuzzy-match tasks, press Enter to edit, or create when nothing matches"),
1223 "",
"",
false, icon_note());
1224 std::vector<TaskManager::TaskRecord> filtered;
1225 const std::string search_text = trim(search);
1226 for (
const auto& task : *tasks) {
1227 const std::string haystack = std::format(
"{} {} {} {}", task.title, task.description,
1228 task.project_title, task.board_title);
1229 if (fuzzy_match(haystack, search_text)) {
1230 filtered.push_back(task);
1233 if (!search_text.empty() && filtered.empty()) {
1234 items.push_back(item(std::format(
"新建任务 / Create task:{}", search_text),
1235 bilingual(
"没有匹配任务,回车进入插入流程",
1236 "No matching task; press Enter to start the insert flow"),
1238 true, icon_task()));
1239 }
else if (!filtered.empty()) {
1240 const json task_rows = task_items(filtered);
1241 for (
const auto& task : task_rows) {
1242 items.push_back(task);
1246 item(
"当前项目 / 清单没有任务",
1247 bilingual(
"继续输入标题即可创建新任务",
"Type a title to create the first task"),
1248 "",
"",
false, icon_task()));
1250 items.push_back(help_item);
1251 items.push_back(item(
"← 返回上一级 / Back",
1252 bilingual(
"返回项目与清单列表,继续选择其他层级",
1253 "Return to the project and list selection"),
1254 "",
"board ",
false, icon_note()));
1255 return feedback(items);
1259 if (query ==
"week") {
1266 json due_items = task_items(*tasks);
1267 due_items.insert(due_items.begin(),
1268 item(
"同步本周到提醒 + OmniFocus",
1269 bilingual(
"任务必须已有截止日期",
"Tasks must already have due dates"),
1270 "sync-week:both",
"",
true, icon_sync()));
1271 due_items.push_back(item(
1273 bilingual(std::format(
"{} → {} · {} 项", bounds.first, bounds.last, tasks->size()),
1274 std::format(
"{} → {} · {} tasks", bounds.first, bounds.last, tasks->size())),
1275 "",
"",
false, icon_note()));
1276 return feedback(due_items);
1280 if (query ==
"timers") {
1283 return feedback(json::array(
1286 json timer_items = json::array();
1287 for (
const auto& timer : *timers) {
1288 timer_items.push_back(
1289 item(std::format(
"▶ {}", timer.title),
1290 bilingual(std::format(
"开始于 {} · {}", timer.started_at, timer.task_id),
1291 std::format(
"started {} · task {}", timer.started_at, timer.task_id)),
1292 std::format(
"timer-stop:{}", timer.task_id),
"",
true, icon_timer()));
1294 if (timer_items.empty()) {
1295 timer_items.push_back(
1297 bilingual(
"从任务详情页启动计时",
"Start a timer from a task detail page"),
"",
"",
1298 false, icon_timer()));
1300 return feedback(timer_items);
1304 if (query.starts_with(
"task:")) {
1305 const std::string
id = trim(query.substr(5));
1311 return task_detail(*task);
1315 for (
const std::string_view command : {
"rename:",
"desc:",
"due:",
"start:",
"priority:"}) {
1316 if (!query.starts_with(command)) {
1319 const std::string remainder = query.substr(command.size());
1320 const auto [id, payload] = split_first(remainder,
' ');
1321 if (trim(payload).empty()) {
1322 const std::string label = command ==
"rename:" ?
"新标题"
1323 : command ==
"desc:" ?
"新描述"
1324 : command ==
"due:" ?
"截止日期"
1325 : command ==
"start:" ?
"开始日期"
1327 const std::string english = command ==
"priority:" ?
"Type low, medium, high or critical"
1328 :
"Type the new value, then press Enter";
1329 return feedback(json::array(
1330 {item(std::format(
"填写{}", label),
1331 bilingual(command ==
"priority:"
1332 ?
"可选:low/低、medium/中、high/高、critical/紧急"
1333 :
"继续输入;日期可用 today、tomorrow、+3d、monday 或 YYYY-MM-DD",
1335 "",
"",
false, icon_info())}));
1337 const std::string action_prefix(command.substr(0, command.size() - 1));
1339 json::array({item(std::format(
"三级:任务|应用更改:{}", trim(payload)),
1340 bilingual(std::format(
"任务 {}",
id), std::format(
"Task {}",
id)),
1342 "",
true, icon_task())}));
1346 if (query.starts_with(
"schedule:")) {
1347 const std::string remainder = query.substr(9);
1348 const auto [id, payload] = split_first(remainder,
' ');
1349 const auto [start, due] = split_first(payload,
'|');
1350 if (trim(start).empty() || trim(due).empty()) {
1352 json::array({error_item(
"格式:开始日期|截止日期",
"Format: start-date|due-date")}));
1355 json::array({item(std::format(
"三级:任务|应用计划:{} → {}", trim(start), trim(due)),
1356 bilingual(std::format(
"任务 {}",
id), std::format(
"Task {}",
id)),
1359 "",
true, icon_week())}));
1363 if (query.starts_with(
"timer:")) {
1364 const std::string
id = trim(query.substr(6));
1370 return feedback(json::array({
1371 item(
"启动计时", bilingual(task->title,
"Start timing this task"),
1372 std::format(
"timer-start:{}",
id),
"",
true, icon_timer()),
1373 item(
"停止并写入 timelog", bilingual(task->title,
"Stop and write a timelog"),
1374 std::format(
"timer-stop:{}",
id),
"",
true, icon_timer()),
1379 if (query.starts_with(
"delete:")) {
1380 const std::string
id = trim(query.substr(7));
1381 return feedback(json::array({item(
1383 bilingual(
"再次回车执行;任务文件移入 tasks/.trash,并从清单移除",
1384 "Press Enter again; move the task to tasks/.trash and remove it from lists"),
1385 std::format(
"delete-confirm:{}",
id),
"",
true, system_icon(
"ToolbarDeleteIcon.icns"))}));
1392 return feedback(json::array(
1395 return feedback(board_items(*boards,
"",
true, query));
1398 return feedback(json::array(
1399 {error_item(
"没有匹配的命令",
"按 Esc 回到 s 总览;可用 ap、rl、v、t、w、tm、i、cf")}));
1400 }
catch (
const std::exception& error) {
1403 json::array({error_item(
"界面数据处理失败", std::format(
"UI error: {}", error.what()))}));