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