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