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;
9
10use crate::*;
11
12#[derive(Debug, Default)]
13/// The `git-iblame` command line interface.
14/// # Examples
15/// ```no_run
16/// use git_iblame::Cli;
17///
18/// # use std::path::PathBuf;
19/// fn main() -> anyhow::Result<()> {
20///   let path = PathBuf::from("path/to/file");
21///   let mut cli: Cli = Cli::new(&path);
22///   cli.run()
23/// }
24/// ```
25pub 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    /// Run the `git-iblame` command line interface.
40    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::PrevDiff => renderer.move_to_prev_diff(),
86            // Command::NextDiff => renderer.move_to_next_diff(),
87            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, /*reverses*/ 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                    // Invalidate because the "working" message cleared the screen.
113                    .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}