1use std::{
2 io::stdout,
3 path::{Path, PathBuf},
4 time::Duration,
5};
6
7use clap::Parser;
8use crossterm::{clipboard::CopyToClipboard, cursor, execute, terminal};
9use git2::Oid;
10use log::debug;
11
12use crate::{blame::FileHistory, extensions::TerminalRawModeScope};
13
14use super::*;
15
16#[derive(Debug, Default, Parser)]
18#[command(version, about)]
19struct Args {
20 #[arg(long, default_value_t = false)]
22 diff_git2: bool,
23
24 path: PathBuf,
26}
27
28#[derive(Debug, Default)]
29pub struct Cli {
42 path: PathBuf,
43 history: Vec<Oid>,
44 last_search: Option<String>,
45}
46
47impl Cli {
48 pub fn new_from_args() -> Self {
49 let args = Args::parse();
50 if args.diff_git2 {
51 crate::blame::FileCommit::use_git2();
52 }
53 Self {
54 path: args.path,
55 ..Default::default()
56 }
57 }
58
59 pub fn new(path: &Path) -> Self {
60 Self {
61 path: path.to_path_buf(),
62 ..Default::default()
63 }
64 }
65
66 pub fn run(&mut self) -> anyhow::Result<()> {
68 let mut history = FileHistory::new(&self.path);
69 history.read_start()?;
70
71 let mut renderer = BlameRenderer::new(history)?;
72 let size = terminal::size()?;
73 renderer.set_view_size((size.0, size.1 - 1));
74
75 let mut ui = CommandUI::new();
76 let mut out = stdout();
77 let mut terminal_raw_mode = TerminalRawModeScope::new_with_alternate_screen()?;
78 loop {
79 let result = renderer.render(&mut out);
80 ui.set_result(result);
81 let command_rows = renderer.rendered_rows();
82
83 if renderer.history().is_reading() {
84 ui.timeout = Duration::from_millis(1000);
85 if matches!(ui.prompt, CommandPrompt::None) {
86 ui.prompt = CommandPrompt::Loading;
87 }
88 } else {
89 ui.timeout = Duration::ZERO;
90 if matches!(ui.prompt, CommandPrompt::Loading) {
91 ui.prompt = CommandPrompt::None;
92 }
93 }
94 let command = ui.read(command_rows)?;
95 match command {
96 Command::Quit => break,
97 Command::Timeout => {}
98 _ => ui.prompt = CommandPrompt::None,
99 }
100 let result = self.handle_command(command, &mut renderer, &mut ui);
101 ui.set_result(result);
102 }
103
104 terminal_raw_mode.reset()?;
105 Ok(())
106 }
107
108 fn handle_command(
109 &mut self,
110 command: Command,
111 renderer: &mut BlameRenderer,
112 ui: &mut CommandUI,
113 ) -> anyhow::Result<()> {
114 let mut out = stdout();
115 match command {
116 Command::PrevLine => renderer.move_to_prev_line_by(1),
117 Command::NextLine => renderer.move_to_next_line_by(1),
118 Command::PrevPage => renderer.move_to_prev_page(),
121 Command::NextPage => renderer.move_to_next_page(),
122 Command::FirstLine => renderer.move_to_first_line(),
123 Command::LastLine => renderer.move_to_last_line(),
124 Command::LineNumber(number) => renderer.set_current_line_number(number)?,
125 Command::Search(search) => {
126 renderer.search(&search, false);
127 self.last_search = Some(search);
128 }
129 Command::SearchPrev | Command::SearchNext => {
130 if let Some(search) = self.last_search.as_ref() {
131 renderer.search(search, command == Command::SearchPrev);
132 }
133 }
134 Command::Older => {
135 let path_before = renderer.path().to_path_buf();
136 let old_commit_id = renderer.commit_id();
137 renderer.set_commit_id_to_older_than_current_line()?;
138 if !old_commit_id.is_zero() {
139 self.history.push(old_commit_id);
140 }
141 if path_before != renderer.path() {
142 ui.set_prompt(format!("Path changed to {}", renderer.path().display()));
143 }
144 }
145 Command::Newer => {
146 if let Some(commit_id) = self.history.pop() {
147 let path_before = renderer.path().to_path_buf();
148 renderer.set_commit_id(commit_id)?;
149 if path_before != renderer.path() {
150 ui.set_prompt(format!("Path changed to {}", renderer.path().display()));
151 }
152 }
153 }
154 Command::Log => {
155 let old_commit_id = renderer.commit_id();
156 renderer.set_log_content()?;
157 if !old_commit_id.is_zero() {
158 self.history.push(old_commit_id);
159 }
160 }
161 Command::Copy => {
162 if let Ok(commit_id) = renderer.current_line_commit_id() {
163 execute!(
164 out,
165 CopyToClipboard::to_clipboard_from(commit_id.to_string())
166 )?;
167 ui.set_prompt("Copied to clipboard".to_string());
168 }
169 }
170 Command::ShowCommit | Command::ShowDiff => {
171 let mut terminal_raw_mode = TerminalRawModeScope::new(false)?;
172 renderer.show_current_line_commit(command == Command::ShowDiff)?;
173 terminal_raw_mode.reset()?;
174 CommandUI::wait_for_any_key("Press any key to continue...")?;
175 }
176 Command::Help => {
177 execute!(
178 out,
179 terminal::Clear(terminal::ClearType::All),
180 cursor::MoveTo(0, 0),
181 )?;
182 renderer.invalidate_render();
183 let mut terminal_raw_mode = TerminalRawModeScope::new(false)?;
184 ui.key_map.print_help();
185 println!();
186 terminal_raw_mode.reset()?;
187 CommandUI::wait_for_any_key("Press any key to continue...")?;
188 }
189 Command::Timeout => renderer.read_poll()?,
190 Command::Repaint => renderer.invalidate_render(),
191 Command::Resize(columns, rows) => renderer.set_view_size((columns, rows - 1)),
192 Command::Debug => {
193 let commit_id = renderer.current_line_commit_id()?;
194 let commit = renderer.history().commits().get_by_commit_id(commit_id)?;
195 debug!("debug_current_line: {commit:?}");
196 }
197 Command::Quit => {}
198 }
199 Ok(())
200 }
201}