git_iblame/ui/
cli.rs

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)]
16/// The `git-iblame` command line interface.
17/// # Examples
18/// ```no_run
19/// use git_iblame::ui::Cli;
20///
21/// # use std::path::PathBuf;
22/// fn main() -> anyhow::Result<()> {
23///   let path = PathBuf::from("path/to/file");
24///   let mut cli: Cli = Cli::new(&path);
25///   cli.run()
26/// }
27/// ```
28pub 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    /// Run the `git-iblame` command line interface.
43    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::PrevDiff => renderer.move_to_prev_diff(),
95            // Command::NextDiff => renderer.move_to_next_diff(),
96            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, /*reverses*/ 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                    // Invalidate because the "working" message cleared the screen.
122                    .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}