git_iblame/
cli.rs

1use std::{
2    io::stdout,
3    path::{Path, PathBuf},
4};
5
6use crossterm::{clipboard::CopyToClipboard, cursor, execute, style, terminal};
7use git2::Oid;
8
9use crate::*;
10
11#[derive(Debug, Default)]
12/// The `git-iblame` command line interface.
13/// # Examples
14/// ```no_run
15/// use git_iblame::Cli;
16///
17/// # use std::path::PathBuf;
18/// fn main() -> anyhow::Result<()> {
19///   let path = PathBuf::from("path/to/file");
20///   let mut cli: Cli = Cli::new(&path);
21///   cli.run()
22/// }
23/// ```
24pub struct Cli {
25    path: PathBuf,
26}
27
28impl Cli {
29    pub fn new(path: &Path) -> Self {
30        Self {
31            path: path.to_path_buf(),
32        }
33    }
34
35    /// Run the `git-iblame` command line interface.
36    pub fn run(&mut self) -> anyhow::Result<()> {
37        let mut renderer = BlameRenderer::new(&self.path)?;
38        let size = terminal::size()?;
39        renderer.set_view_size((size.0, size.1 - 1));
40        renderer.read()?;
41
42        let mut history: Vec<Oid> = vec![];
43        let mut last_search: Option<String> = None;
44        let mut out = stdout();
45        let key_map = CommandKeyMap::new();
46        let mut prompt: CommandPrompt = CommandPrompt::None;
47        let mut terminal_raw_mode = TerminalRawModeScope::new(true)?;
48        loop {
49            renderer.render(&mut out)?;
50            let command_rows = renderer.rendered_rows();
51
52            let command = Command::read(command_rows, &key_map, &prompt)?;
53            prompt = CommandPrompt::None;
54            match command {
55                Command::PrevDiff => renderer.move_to_prev_diff(),
56                Command::NextDiff => renderer.move_to_next_diff(),
57                Command::PrevPage => renderer.move_to_prev_page(),
58                Command::NextPage => renderer.move_to_next_page(),
59                Command::FirstLine => renderer.move_to_first_line(),
60                Command::LastLine => renderer.move_to_last_line(),
61                Command::LineNumber(number) => renderer.set_current_line_number(number),
62                Command::Search(search) => {
63                    renderer.search(&search, /*reverses*/ false);
64                    last_search = Some(search);
65                }
66                Command::SearchPrev | Command::SearchNext => {
67                    if let Some(search) = last_search.as_ref() {
68                        renderer.search(search, command == Command::SearchPrev);
69                    }
70                }
71                Command::Older => {
72                    execute!(
73                        out,
74                        terminal::Clear(terminal::ClearType::All),
75                        cursor::MoveTo(0, 0),
76                        style::Print("Working...")
77                    )?;
78                    let path_before = renderer.path().to_path_buf();
79                    let old_commit_id = renderer.commit_id();
80                    if let Err(error) = renderer.set_commit_id_to_older_than_current_line() {
81                        prompt = CommandPrompt::Err { error };
82                        // Invalidate because the "working" message cleared the screen.
83                        renderer.invalidate_render();
84                        continue;
85                    }
86                    history.push(old_commit_id);
87                    if path_before != renderer.path() {
88                        prompt = CommandPrompt::Message {
89                            message: format!("Path changed to {}", renderer.path().display()),
90                        };
91                    }
92                }
93                Command::Newer => {
94                    if let Some(commit_id) = history.pop() {
95                        renderer.set_commit_id(commit_id)?;
96                    }
97                }
98                Command::Copy => {
99                    execute!(
100                        out,
101                        CopyToClipboard::to_clipboard_from(
102                            renderer.current_line_commit_id().to_string()
103                        )
104                    )?;
105                    prompt = CommandPrompt::Message {
106                        message: "Copied to clipboard".to_string(),
107                    };
108                }
109                Command::ShowCommit | Command::ShowDiff => {
110                    let mut terminal_raw_mode = TerminalRawModeScope::new(false)?;
111                    renderer.show_current_line_commit(command == Command::ShowDiff)?;
112                    terminal_raw_mode.reset();
113                    Command::wait_for_any_key("Press any key to continue...")?;
114                }
115                Command::Help => {
116                    execute!(
117                        out,
118                        terminal::Clear(terminal::ClearType::All),
119                        cursor::MoveTo(0, 0),
120                    )?;
121                    renderer.invalidate_render();
122                    let mut terminal_raw_mode = TerminalRawModeScope::new(false)?;
123                    key_map.print_help();
124                    println!();
125                    terminal_raw_mode.reset();
126                    Command::wait_for_any_key("Press any key to continue...")?;
127                }
128                Command::Repaint => renderer.invalidate_render(),
129                Command::Resize(columns, rows) => renderer.set_view_size((columns, rows - 1)),
130                Command::Quit => break,
131            }
132        }
133
134        terminal_raw_mode.reset();
135        Ok(())
136    }
137}