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