1use crate::config::YamlConfig;
2use crate::constants::{section, config_key, search_flag, rmeta_action, REPORT_DATE_FORMAT, REPORT_SIMPLE_DATE_FORMAT, DEFAULT_CHECK_LINES};
3use crate::util::fuzzy;
4use crate::{error, info, usage};
5use chrono::{Local, NaiveDate};
6use colored::Colorize;
7use std::fs;
8use std::io::{Read, Seek, SeekFrom};
9use std::path::Path;
10use std::process::Command;
11
12const DATE_FORMAT: &str = REPORT_DATE_FORMAT;
13const SIMPLE_DATE_FORMAT: &str = REPORT_SIMPLE_DATE_FORMAT;
14
15pub fn handle_report(sub: &str, content: &[String], config: &mut YamlConfig) {
19 if content.is_empty() {
20 if sub == "reportctl" {
21 usage!("j reportctl new [date] | j reportctl sync [date] | j reportctl push | j reportctl pull | j reportctl set-url <url> | j reportctl open");
22 return;
23 }
24 handle_report_tui(config);
26 return;
27 }
28
29 let first = content[0].as_str();
30
31 if sub == "reportctl" {
33 match first {
34 f if f == rmeta_action::NEW => {
35 let date_str = content.get(1).map(|s| s.as_str());
36 handle_week_update(date_str, config);
37 }
38 f if f == rmeta_action::SYNC => {
39 let date_str = content.get(1).map(|s| s.as_str());
40 handle_sync(date_str, config);
41 }
42 f if f == rmeta_action::PUSH => {
43 let msg = content.get(1).map(|s| s.as_str());
44 handle_push(msg, config);
45 }
46 f if f == rmeta_action::PULL => {
47 handle_pull(config);
48 }
49 f if f == rmeta_action::SET_URL => {
50 let url = content.get(1).map(|s| s.as_str());
51 handle_set_url(url, config);
52 }
53 f if f == rmeta_action::OPEN => {
54 handle_open_report(config);
55 }
56 _ => {
57 error!("❌ 未知的元数据操作: {},可选: {}, {}, {}, {}, {}, {}", first, rmeta_action::NEW, rmeta_action::SYNC, rmeta_action::PUSH, rmeta_action::PULL, rmeta_action::SET_URL, rmeta_action::OPEN);
58 }
59 }
60 return;
61 }
62
63 let text = content.join(" ");
65 let text = text.trim().trim_matches('"').to_string();
66
67 if text.is_empty() {
68 error!("⚠️ 内容为空,无法写入");
69 return;
70 }
71
72 handle_daily_report(&text, config);
73}
74
75fn get_report_path(config: &YamlConfig) -> Option<String> {
77 let report_path = config.report_file_path();
78
79 if let Some(parent) = report_path.parent() {
81 let _ = fs::create_dir_all(parent);
82 }
83
84 if !report_path.exists() {
86 if let Err(e) = fs::write(&report_path, "") {
87 error!("❌ 创建日报文件失败: {}", e);
88 return None;
89 }
90 info!("📄 已自动创建日报文件: {:?}", report_path);
91 }
92
93 Some(report_path.to_string_lossy().to_string())
94}
95
96fn get_settings_json_path(report_path: &str) -> std::path::PathBuf {
98 Path::new(report_path).parent().unwrap().join("settings.json")
99}
100
101fn handle_report_tui(config: &mut YamlConfig) {
103 let report_path = match get_report_path(config) {
104 Some(p) => p,
105 None => return,
106 };
107
108 let config_path = get_settings_json_path(&report_path);
109 load_config_from_json_and_sync(&config_path, config);
110
111 let now = Local::now().date_naive();
113 let week_num = config
114 .get_property(section::REPORT, config_key::WEEK_NUM)
115 .and_then(|s| s.parse::<i32>().ok())
116 .unwrap_or(1);
117 let last_day_str = config
118 .get_property(section::REPORT, config_key::LAST_DAY)
119 .cloned()
120 .unwrap_or_default();
121 let last_day = parse_date(&last_day_str);
122
123 let context_lines = 3;
125 let report_file = Path::new(&report_path);
126 let last_lines = read_last_n_lines(report_file, context_lines);
127
128 let mut initial_lines: Vec<String> = last_lines.clone();
130
131 if let Some(last_day) = last_day {
133 if now > last_day {
134 let next_last_day = now + chrono::Duration::days(6);
135 let new_week_title = format!(
136 "# Week{}[{}-{}]",
137 week_num,
138 now.format(DATE_FORMAT),
139 next_last_day.format(DATE_FORMAT)
140 );
141 update_config_files(week_num + 1, &next_last_day, &config_path, config);
142 initial_lines.push(new_week_title);
144 }
145 }
146
147 let today_str = now.format(SIMPLE_DATE_FORMAT);
149 let date_prefix = format!("- 【{}】 ", today_str);
150 initial_lines.push(date_prefix);
151
152 match crate::tui::editor::open_multiline_editor_with_content("📝 编辑日报", &initial_lines) {
154 Ok(Some(text)) => {
155 let original_context_count = last_lines.len();
158
159 replace_last_n_lines(report_file, original_context_count, &text);
161
162 info!("✅ 日报已写入:{}", report_path);
163 }
164 Ok(None) => {
165 info!("已取消编辑");
166 }
169 Err(e) => {
170 error!("❌ 编辑器启动失败: {}", e);
171 }
172 }
173}
174
175fn replace_last_n_lines(path: &Path, n: usize, new_content: &str) {
177 let content = match fs::read_to_string(path) {
178 Ok(c) => c,
179 Err(e) => {
180 error!("❌ 读取文件失败: {}", e);
181 return;
182 }
183 };
184
185 let all_lines: Vec<&str> = content.lines().collect();
186
187 let keep_count = if all_lines.len() > n {
189 all_lines.len() - n
190 } else {
191 0
192 };
193
194 let mut result = String::new();
195
196 for line in &all_lines[..keep_count] {
198 result.push_str(line);
199 result.push('\n');
200 }
201
202 result.push_str(new_content);
204
205 if !result.ends_with('\n') {
207 result.push('\n');
208 }
209
210 if let Err(e) = fs::write(path, &result) {
211 error!("❌ 写入文件失败: {}", e);
212 }
213}
214
215fn handle_daily_report(content: &str, config: &mut YamlConfig) {
217 let report_path = match get_report_path(config) {
218 Some(p) => p,
219 None => return,
220 };
221
222 info!("📂 日报文件路径:{}", report_path);
223
224 let report_file = Path::new(&report_path);
225 let config_path = get_settings_json_path(&report_path);
226
227 load_config_from_json_and_sync(&config_path, config);
228
229 let now = Local::now().date_naive();
230
231 let week_num = config
232 .get_property(section::REPORT, config_key::WEEK_NUM)
233 .and_then(|s| s.parse::<i32>().ok())
234 .unwrap_or(1);
235
236 let last_day_str = config
237 .get_property(section::REPORT, config_key::LAST_DAY)
238 .cloned()
239 .unwrap_or_default();
240
241 let last_day = parse_date(&last_day_str);
242
243 match last_day {
244 Some(last_day) => {
245 if now > last_day {
246 let next_last_day = now + chrono::Duration::days(6);
248 let new_week_title = format!(
249 "# Week{}[{}-{}]\n",
250 week_num,
251 now.format(DATE_FORMAT),
252 next_last_day.format(DATE_FORMAT)
253 );
254 update_config_files(week_num + 1, &next_last_day, &config_path, config);
255 append_to_file(report_file, &new_week_title);
256 }
257 }
258 None => {
259 error!("❌ 无法解析 last_day 日期: {}", last_day_str);
260 return;
261 }
262 }
263
264 let today_str = now.format(SIMPLE_DATE_FORMAT);
265 let log_entry = format!("- 【{}】 {}\n", today_str, content);
266 append_to_file(report_file, &log_entry);
267 info!("✅ 成功将内容写入:{}", report_path);
268}
269
270fn handle_week_update(date_str: Option<&str>, config: &mut YamlConfig) {
272 let report_path = match get_report_path(config) {
273 Some(p) => p,
274 None => return,
275 };
276
277 let config_path = get_settings_json_path(&report_path);
278
279 let week_num = config
280 .get_property(section::REPORT, config_key::WEEK_NUM)
281 .and_then(|s| s.parse::<i32>().ok())
282 .unwrap_or(1);
283
284 let last_day_str = date_str
285 .map(|s| s.to_string())
286 .or_else(|| config.get_property(section::REPORT, config_key::LAST_DAY).cloned())
287 .unwrap_or_default();
288
289 match parse_date(&last_day_str) {
290 Some(last_day) => {
291 let next_last_day = last_day + chrono::Duration::days(7);
292 update_config_files(week_num + 1, &next_last_day, &config_path, config);
293 }
294 None => {
295 error!("❌ 更新周数失败,请检查日期字符串是否有误: {}", last_day_str);
296 }
297 }
298}
299
300fn handle_sync(date_str: Option<&str>, config: &mut YamlConfig) {
302 let report_path = match get_report_path(config) {
303 Some(p) => p,
304 None => return,
305 };
306
307 let config_path = get_settings_json_path(&report_path);
308
309 load_config_from_json_and_sync(&config_path, config);
310
311 let week_num = config
312 .get_property(section::REPORT, config_key::WEEK_NUM)
313 .and_then(|s| s.parse::<i32>().ok())
314 .unwrap_or(1);
315
316 let last_day_str = date_str
317 .map(|s| s.to_string())
318 .or_else(|| config.get_property(section::REPORT, config_key::LAST_DAY).cloned())
319 .unwrap_or_default();
320
321 match parse_date(&last_day_str) {
322 Some(last_day) => {
323 update_config_files(week_num, &last_day, &config_path, config);
324 }
325 None => {
326 error!("❌ 更新周数失败,请检查日期字符串是否有误: {}", last_day_str);
327 }
328 }
329}
330
331fn update_config_files(
333 week_num: i32,
334 last_day: &NaiveDate,
335 config_path: &Path,
336 config: &mut YamlConfig,
337) {
338 let last_day_str = last_day.format(DATE_FORMAT).to_string();
339
340 config.set_property(section::REPORT, config_key::WEEK_NUM, &week_num.to_string());
342 config.set_property(section::REPORT, config_key::LAST_DAY, &last_day_str);
343 info!(
344 "✅ 更新YAML配置文件成功:周数 = {}, 周结束日期 = {}",
345 week_num, last_day_str
346 );
347
348 if config_path.exists() {
350 let json = serde_json::json!({
351 "week_num": week_num,
352 "last_day": last_day_str
353 });
354 match fs::write(config_path, json.to_string()) {
355 Ok(_) => info!(
356 "✅ 更新JSON配置文件成功:周数 = {}, 周结束日期 = {}",
357 week_num, last_day_str
358 ),
359 Err(e) => error!("❌ 更新JSON配置文件时出错: {}", e),
360 }
361 }
362}
363
364fn load_config_from_json_and_sync(config_path: &Path, config: &mut YamlConfig) {
366 if !config_path.exists() {
367 error!("❌ 日报配置文件不存在:{:?}", config_path);
368 return;
369 }
370
371 match fs::read_to_string(config_path) {
372 Ok(content) => {
373 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
374 let last_day = json
375 .get("last_day")
376 .and_then(|v| v.as_str())
377 .unwrap_or("");
378 let week_num = json.get("week_num").and_then(|v| v.as_i64()).unwrap_or(1);
379
380 info!(
381 "✅ 从日报配置文件中读取到:last_day = {}, week_num = {}",
382 last_day, week_num
383 );
384
385 if let Some(last_day_date) = parse_date(last_day) {
386 update_config_files(week_num as i32, &last_day_date, config_path, config);
387 }
388 } else {
389 error!("❌ 解析日报配置文件时出错");
390 }
391 }
392 Err(e) => error!("❌ 读取日报配置文件失败: {}", e),
393 }
394}
395
396fn parse_date(s: &str) -> Option<NaiveDate> {
397 NaiveDate::parse_from_str(s, DATE_FORMAT).ok()
398}
399
400fn append_to_file(path: &Path, content: &str) {
401 use std::fs::OpenOptions;
402 use std::io::Write;
403 match OpenOptions::new().create(true).append(true).open(path) {
404 Ok(mut f) => {
405 if let Err(e) = f.write_all(content.as_bytes()) {
406 error!("❌ 写入文件失败: {}", e);
407 }
408 }
409 Err(e) => error!("❌ 打开文件失败: {}", e),
410 }
411}
412
413fn handle_open_report(config: &YamlConfig) {
417 let report_path = match get_report_path(config) {
418 Some(p) => p,
419 None => return,
420 };
421
422 let path = Path::new(&report_path);
423 if !path.is_file() {
424 error!("❌ 日报文件不存在: {}", report_path);
425 return;
426 }
427
428 let content = match fs::read_to_string(path) {
430 Ok(c) => c,
431 Err(e) => {
432 error!("❌ 读取日报文件失败: {}", e);
433 return;
434 }
435 };
436
437 let lines: Vec<String> = content.lines().map(|l| l.to_string()).collect();
438
439 match crate::tui::editor::open_multiline_editor_with_content("📝 编辑日报文件", &lines) {
441 Ok(Some(text)) => {
442 let mut result = text;
444 if !result.ends_with('\n') {
445 result.push('\n');
446 }
447 if let Err(e) = fs::write(path, &result) {
448 error!("❌ 写入日报文件失败: {}", e);
449 return;
450 }
451 info!("✅ 日报文件已保存:{}", report_path);
452 }
453 Ok(None) => {
454 info!("已取消编辑,文件未修改");
455 }
456 Err(e) => {
457 error!("❌ 编辑器启动失败: {}", e);
458 }
459 }
460}
461
462fn handle_set_url(url: Option<&str>, config: &mut YamlConfig) {
466 match url {
467 Some(u) if !u.is_empty() => {
468 let old = config.get_property(section::REPORT, config_key::GIT_REPO).cloned();
469 config.set_property(section::REPORT, config_key::GIT_REPO, u);
470
471 if let Some(dir) = get_report_dir(config) {
473 let git_dir = Path::new(&dir).join(".git");
474 if git_dir.exists() {
475 sync_git_remote(config);
476 }
477 }
478
479 match old {
480 Some(old_url) if !old_url.is_empty() => {
481 info!("✅ git 仓库地址已更新: {} → {}", old_url, u);
482 }
483 _ => {
484 info!("✅ git 仓库地址已设置: {}", u);
485 }
486 }
487 }
488 _ => {
489 match config.get_property(section::REPORT, config_key::GIT_REPO) {
491 Some(url) if !url.is_empty() => {
492 info!("📦 当前 git 仓库地址: {}", url);
493 }
494 _ => {
495 info!("📦 尚未配置 git 仓库地址");
496 usage!("reportctl set-url <repo_url>");
497 }
498 }
499 }
500 }
501}
502
503fn get_report_dir(config: &YamlConfig) -> Option<String> {
507 let report_path = config.report_file_path();
508 report_path.parent().map(|p| p.to_string_lossy().to_string())
509}
510
511fn run_git_in_report_dir(args: &[&str], config: &YamlConfig) -> Option<std::process::ExitStatus> {
513 let dir = match get_report_dir(config) {
514 Some(d) => d,
515 None => {
516 error!("❌ 无法确定日报目录");
517 return None;
518 }
519 };
520
521 let result = Command::new("git")
522 .args(args)
523 .current_dir(&dir)
524 .status();
525
526 match result {
527 Ok(status) => Some(status),
528 Err(e) => {
529 error!("💥 执行 git 命令失败: {}", e);
530 None
531 }
532 }
533}
534
535fn ensure_git_repo(config: &YamlConfig) -> bool {
537 let dir = match get_report_dir(config) {
538 Some(d) => d,
539 None => {
540 error!("❌ 无法确定日报目录");
541 return false;
542 }
543 };
544
545 let git_dir = Path::new(&dir).join(".git");
546 if git_dir.exists() {
547 sync_git_remote(config);
549 return true;
550 }
551
552 let git_repo = config.get_property(section::REPORT, config_key::GIT_REPO);
554 if git_repo.is_none() || git_repo.unwrap().is_empty() {
555 error!("❌ 尚未配置 git 仓库地址,请先执行: j reportctl set-url <repo_url>");
556 return false;
557 }
558 let repo_url = git_repo.unwrap().clone();
559
560 info!("📦 日报目录尚未初始化 git 仓库,正在初始化...");
561
562 if let Some(status) = run_git_in_report_dir(&["init", "-b", "main"], config) {
564 if !status.success() {
565 error!("❌ git init 失败");
566 return false;
567 }
568 } else {
569 return false;
570 }
571
572 if let Some(status) = run_git_in_report_dir(&["remote", "add", "origin", &repo_url], config) {
574 if !status.success() {
575 error!("❌ git remote add 失败");
576 return false;
577 }
578 } else {
579 return false;
580 }
581
582 info!("✅ git 仓库初始化完成,remote: {}", repo_url);
583 true
584}
585
586fn sync_git_remote(config: &YamlConfig) {
588 let git_repo = match config.get_property(section::REPORT, config_key::GIT_REPO) {
589 Some(url) if !url.is_empty() => url.clone(),
590 _ => return, };
592
593 let dir = match get_report_dir(config) {
595 Some(d) => d,
596 None => return,
597 };
598
599 let current_url = Command::new("git")
600 .args(["remote", "get-url", "origin"])
601 .current_dir(&dir)
602 .output();
603
604 match current_url {
605 Ok(output) if output.status.success() => {
606 let url = String::from_utf8_lossy(&output.stdout).trim().to_string();
607 if url != git_repo {
608 let _ = run_git_in_report_dir(&["remote", "set-url", "origin", &git_repo], config);
610 info!("🔄 已同步 remote origin: {} → {}", url, git_repo);
611 }
612 }
613 _ => {
614 let _ = run_git_in_report_dir(&["remote", "add", "origin", &git_repo], config);
616 }
617 }
618}
619
620fn handle_push(commit_msg: Option<&str>, config: &YamlConfig) {
622 let git_repo = config.get_property(section::REPORT, config_key::GIT_REPO);
624 if git_repo.is_none() || git_repo.unwrap().is_empty() {
625 error!("❌ 尚未配置 git 仓库地址,请先执行: j reportctl set-url <repo_url>");
626 return;
627 }
628
629 if !ensure_git_repo(config) {
631 return;
632 }
633
634 let default_msg = format!("update report {}", Local::now().format("%Y-%m-%d %H:%M"));
635 let msg = commit_msg.unwrap_or(&default_msg);
636
637 info!("📤 正在推送周报到远程仓库...");
638
639 if let Some(status) = run_git_in_report_dir(&["add", "."], config) {
641 if !status.success() {
642 error!("❌ git add 失败");
643 return;
644 }
645 } else {
646 return;
647 }
648
649 if let Some(status) = run_git_in_report_dir(&["commit", "-m", msg], config) {
651 if !status.success() {
652 info!("ℹ️ git commit 返回非零退出码(可能没有新变更)");
654 }
655 } else {
656 return;
657 }
658
659 if let Some(status) = run_git_in_report_dir(&["push", "-u", "origin", "main"], config) {
661 if status.success() {
662 info!("✅ 周报已成功推送到远程仓库");
663 } else {
664 error!("❌ git push 失败,请检查网络连接和仓库权限");
665 }
666 }
667}
668
669fn handle_pull(config: &YamlConfig) {
671 let git_repo = config.get_property(section::REPORT, config_key::GIT_REPO);
673 if git_repo.is_none() || git_repo.unwrap().is_empty() {
674 error!("❌ 尚未配置 git 仓库地址,请先执行: j reportctl set-url <repo_url>");
675 return;
676 }
677
678 let dir = match get_report_dir(config) {
679 Some(d) => d,
680 None => {
681 error!("❌ 无法确定日报目录");
682 return;
683 }
684 };
685
686 let git_dir = Path::new(&dir).join(".git");
687
688 if !git_dir.exists() {
689 let repo_url = git_repo.unwrap().clone();
691 info!("📥 日报目录尚未初始化,正在从远程仓库克隆...");
692
693 let report_path = config.report_file_path();
695 let has_existing = report_path.exists() && fs::metadata(&report_path).map(|m| m.len() > 0).unwrap_or(false);
696
697 if has_existing {
698 let backup_path = report_path.with_extension("md.bak");
700 if let Err(e) = fs::copy(&report_path, &backup_path) {
701 error!("⚠️ 备份现有日报文件失败: {}", e);
702 } else {
703 info!("📋 已备份现有日报到: {:?}", backup_path);
704 }
705 }
706
707 let temp_dir = Path::new(&dir).with_file_name(".report_clone_tmp");
710 let _ = fs::remove_dir_all(&temp_dir);
711
712 let result = Command::new("git")
713 .args(["clone", "-b", "main", &repo_url, &temp_dir.to_string_lossy()])
714 .status();
715
716 match result {
717 Ok(status) if status.success() => {
718 let _ = fs::remove_dir_all(&dir);
720 if let Err(e) = fs::rename(&temp_dir, &dir) {
721 error!("❌ 移动克隆仓库失败: {},临时目录: {:?}", e, temp_dir);
722 return;
723 }
724 info!("✅ 成功从远程仓库克隆周报");
725 }
726 Ok(_) => {
727 error!("❌ git clone 失败,请检查仓库地址和网络连接");
728 let _ = fs::remove_dir_all(&temp_dir);
729 }
730 Err(e) => {
731 error!("💥 执行 git clone 失败: {}", e);
732 let _ = fs::remove_dir_all(&temp_dir);
733 }
734 }
735 } else {
736 sync_git_remote(config);
738
739 let has_commits = Command::new("git")
741 .args(["rev-parse", "HEAD"])
742 .current_dir(&dir)
743 .output()
744 .map(|o| o.status.success())
745 .unwrap_or(false);
746
747 if !has_commits {
748 info!("📥 本地仓库尚无提交,正在从远程仓库拉取...");
750
751 let report_path = config.report_file_path();
753 if report_path.exists() && fs::metadata(&report_path).map(|m| m.len() > 0).unwrap_or(false) {
754 let backup_path = report_path.with_extension("md.bak");
755 let _ = fs::copy(&report_path, &backup_path);
756 info!("📋 已备份本地日报到: {:?}", backup_path);
757 }
758
759 if let Some(status) = run_git_in_report_dir(&["fetch", "origin", "main"], config) {
761 if !status.success() {
762 error!("❌ git fetch 失败,请检查网络连接和仓库地址");
763 return;
764 }
765 } else {
766 return;
767 }
768
769 if let Some(status) = run_git_in_report_dir(&["reset", "--hard", "origin/main"], config) {
771 if status.success() {
772 info!("✅ 成功从远程仓库拉取周报");
773 } else {
774 error!("❌ git reset 失败");
775 }
776 }
777 } else {
778 info!("📥 正在从远程仓库拉取最新周报...");
780
781 let _ = run_git_in_report_dir(&["add", "-A"], config);
783 let stash_result = Command::new("git")
784 .args(["stash", "push", "-m", "auto-stash-before-pull"])
785 .current_dir(&dir)
786 .output();
787 let has_stash = match &stash_result {
788 Ok(output) => {
789 let msg = String::from_utf8_lossy(&output.stdout);
790 !msg.contains("No local changes")
791 }
792 Err(_) => false,
793 };
794
795 let pull_ok = if let Some(status) = run_git_in_report_dir(&["pull", "origin", "main", "--rebase"], config) {
797 if status.success() {
798 info!("✅ 周报已更新到最新版本");
799 true
800 } else {
801 error!("❌ git pull 失败,请检查网络连接或手动解决冲突");
802 false
803 }
804 } else {
805 false
806 };
807
808 if has_stash {
810 if let Some(status) = run_git_in_report_dir(&["stash", "pop"], config) {
811 if !status.success() && pull_ok {
812 info!("⚠️ stash pop 存在冲突,请手动合并本地修改(已保存在 git stash 中)");
813 }
814 }
815 }
816 }
817 }
818}
819
820pub fn handle_check(line_count: Option<&str>, config: &YamlConfig) {
824 if line_count == Some("open") {
826 handle_open_report(config);
827 return;
828 }
829
830 let num = match line_count {
831 Some(s) => match s.parse::<usize>() {
832 Ok(n) if n > 0 => n,
833 _ => {
834 error!("❌ 无效的行数参数: {},请输入正整数或 open", s);
835 return;
836 }
837 },
838 None => DEFAULT_CHECK_LINES,
839 };
840
841 let report_path = match get_report_path(config) {
842 Some(p) => p,
843 None => return,
844 };
845
846 info!("📂 正在读取周报文件路径: {}", report_path);
847
848 let path = Path::new(&report_path);
849 if !path.is_file() {
850 error!("❌ 文件不存在或不是有效文件: {}", report_path);
851 return;
852 }
853
854 let lines = read_last_n_lines(path, num);
855 info!("📄 最近的 {} 行内容如下:", lines.len());
856 let md_content = lines.join("\n");
858 crate::md!("{}", md_content);
859}
860
861pub fn handle_search(line_count: &str, target: &str, fuzzy_flag: Option<&str>, config: &YamlConfig) {
865 let num = if line_count == "all" {
866 usize::MAX
867 } else {
868 match line_count.parse::<usize>() {
869 Ok(n) if n > 0 => n,
870 _ => {
871 error!("❌ 无效的行数参数: {},请输入正整数或 all", line_count);
872 return;
873 }
874 }
875 };
876
877 let report_path = match get_report_path(config) {
878 Some(p) => p,
879 None => return,
880 };
881
882 info!("📂 正在读取周报文件路径: {}", report_path);
883
884 let path = Path::new(&report_path);
885 if !path.is_file() {
886 error!("❌ 文件不存在或不是有效文件: {}", report_path);
887 return;
888 }
889
890 let is_fuzzy = matches!(fuzzy_flag, Some(f) if f == search_flag::FUZZY_SHORT || f == search_flag::FUZZY);
891 if is_fuzzy {
892 info!("启用模糊匹配...");
893 }
894
895 let lines = read_last_n_lines(path, num);
896 info!("🔍 搜索目标关键字: {}", target.green());
897
898 let mut index = 0;
899 for line in &lines {
900 let matched = if is_fuzzy {
901 fuzzy::fuzzy_match(line, target)
902 } else {
903 line.contains(target)
904 };
905
906 if matched {
907 index += 1;
908 let highlighted = fuzzy::highlight_matches(line, target, is_fuzzy);
909 info!("[{}] {}", index, highlighted);
910 }
911 }
912
913 if index == 0 {
914 info!("nothing found 😢");
915 }
916}
917
918fn read_last_n_lines(path: &Path, n: usize) -> Vec<String> {
920 let mut lines = Vec::new();
921 let buffer_size: usize = 16384; let mut file = match fs::File::open(path) {
924 Ok(f) => f,
925 Err(e) => {
926 error!("❌ 读取文件时发生错误: {}", e);
927 return lines;
928 }
929 };
930
931 let file_len = match file.metadata() {
932 Ok(m) => m.len() as usize,
933 Err(_) => return lines,
934 };
935
936 if file_len == 0 {
937 return lines;
938 }
939
940 if n == usize::MAX || file_len <= buffer_size * 2 {
942 let mut content = String::new();
943 let _ = file.seek(SeekFrom::Start(0));
944 if file.read_to_string(&mut content).is_ok() {
945 let all_lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
946 if n >= all_lines.len() {
947 return all_lines;
948 }
949 return all_lines[all_lines.len() - n..].to_vec();
950 }
951 return lines;
952 }
953
954 let mut pointer = file_len;
956 let mut remainder = Vec::new();
957
958 while pointer > 0 && lines.len() < n {
959 let bytes_to_read = pointer.min(buffer_size);
960 pointer -= bytes_to_read;
961
962 let _ = file.seek(SeekFrom::Start(pointer as u64));
963 let mut buffer = vec![0u8; bytes_to_read];
964 if file.read_exact(&mut buffer).is_err() {
965 break;
966 }
967
968 buffer.extend(remainder.drain(..));
970
971 let text = String::from_utf8_lossy(&buffer).to_string();
973 let mut block_lines: Vec<&str> = text.split('\n').collect();
974
975 if pointer > 0 {
977 remainder = block_lines.remove(0).as_bytes().to_vec();
978 }
979
980 for line in block_lines.into_iter().rev() {
981 if !line.is_empty() {
982 lines.push(line.to_string());
983 if lines.len() >= n {
984 break;
985 }
986 }
987 }
988 }
989
990 if !remainder.is_empty() && lines.len() < n {
992 let line = String::from_utf8_lossy(&remainder).to_string();
993 if !line.is_empty() {
994 lines.push(line);
995 }
996 }
997
998 lines.reverse();
999 lines
1000}