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