Skip to main content

gitu/
lib.rs

1pub mod app;
2mod bindings;
3mod calling_process;
4mod capped_input;
5pub mod cli;
6mod cmd_log;
7pub mod config;
8mod diff_renderer;
9pub mod error;
10mod file_watcher;
11mod git;
12pub mod gitu_diff;
13mod highlight;
14mod item_data;
15mod items;
16mod key_parser;
17mod menu;
18mod ops;
19pub mod picker;
20mod prompt;
21mod rebase_todo;
22mod screen;
23mod search;
24pub mod style;
25mod syntax_parser;
26pub mod term;
27#[cfg(test)]
28mod tests;
29mod text_input;
30mod ui;
31
32use bindings::Bindings;
33use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventState, KeyModifiers};
34use error::Error;
35use git2::Repository;
36use items::Item;
37use ops::Action;
38use std::{
39    io::{self, IsTerminal},
40    path::{Path, PathBuf},
41    process::Command,
42    rc::Rc,
43    sync::Arc,
44    time::Duration,
45};
46use term::Term;
47
48use crate::capped_input::Capped;
49use crate::config::Config;
50use crate::screen::pager::Paged;
51
52pub const LOG_FILE_NAME: &str = "gitu.log";
53
54//                                An overview of Gitu's ui and terminology:
55//
56//                Screen (src/screen/*)
57//                  │
58//                  ▼
59//                 ┌──────────────────────────────────────────────────────────────────┐
60//        Item───┬─► On branch master                                                 │
61//        Item   └─► Your branch is up to date with 'origin/master'.                  │
62//        ...      │                                                                  │
63//                 │ Untracked files                                                  │
64//                 │ src/tests/rebase.rs                                              │
65//                 │                                                                  │
66//                 │ Unstaged changes (4)                                             │
67//                 │▌modified   src/keybinds.rs…                                      │
68//                 │ modified   src/ops/mod.rs…                                       │
69//                 │ modified   src/ops/rebase.rs…                                    │
70//                 │ modified   src/tests/mod.rs…                                     │
71//                 │                                                                  │
72//                 │ Stashes                                                          │
73//                 │ stash@0 On master: scroll                                        │
74//                 │ stash@1 WIP on fix/run-cmd-error-on-bad-exit: 098d14a feat: prom…│
75//                 │                                                                  │
76//                 │ Recent commits                                                   │
77// Ops (src/ops/*) ├──────────────────────────────────────────────────────────────────┤
78//       │         │Help                        Submenu      modified   src/keybinds.r│
79//       └─────┬───►g Refresh                   h Help       ret Show                 │
80//             └───►tab Toggle section          b Branch     K Discard                │
81//                 │k p ↑ Move up               c Commit     s Stage                  │
82//                 │j n ↓ Move down             f Fetch      u Unstage                │
83// Submenu ───────►│C-k C-p C-↑ Move up line    l Log                                 │
84//                 │C-j C-n C-↓ Move down line  F Pull                                │
85//                 │C-u Half page up            P Push                                │
86//                 │C-d Half page down          r Rebase                              │
87//                 │y Show refs                 X Reset                               │
88//                 │                            z Stash                               │
89//                 └──────────────────────────────────────────────────────────────────┘
90
91pub type Res<T> = Result<T, Error>;
92
93/// Run gitu, returning the status to exit with.
94pub fn run(config: Arc<Config>, args: &cli::Args, term: &mut Term) -> Res<i32> {
95    // Before anything slower, and before reading the input: the git process on
96    // the other end of a pipe exits as soon as its output fits in the pipe, and
97    // cannot be found once it has. Reading to EOF is always too late — EOF is
98    // git closing the pipe.
99    let git_argv = args.pager.then(calling_process::paging_for).flatten();
100    log::debug!("Paging the output of {git_argv:?}");
101
102    let dir = find_git_dir()?;
103    let repo = open_repo(&dir)?;
104
105    let piped = args
106        .pager
107        .then(|| read_piped_input(git_argv, config.general.max_input_bytes))
108        .transpose()?;
109
110    let mut app = app::App::create(
111        Rc::new(repo),
112        term.size().map_err(Error::Term)?,
113        args,
114        Arc::clone(&config),
115        true,
116        piped,
117    )?;
118
119    if args.quit_if_one_screen && app.print_if_one_screen(term)? {
120        return Ok(app.state.exit_code);
121    }
122
123    if args.print {
124        return run_app(&mut app, args, term);
125    }
126
127    term.setup_term(&config).map_err(Error::Term)?;
128    let result = run_app(&mut app, args, term);
129    term.reset_term(&config).map_err(Error::Term)?;
130    result
131}
132
133fn run_app(app: &mut app::App, args: &cli::Args, term: &mut Term) -> Res<i32> {
134    if let Some(keys_string) = &args.keys {
135        let ("", keys) = key_parser::parse_keys(keys_string).expect("Couldn't parse keys") else {
136            panic!("Couldn't parse keys");
137        };
138
139        for event in keys_to_events(&keys) {
140            app.handle_event(term, event)?;
141        }
142    }
143
144    app.redraw_now(term)?;
145
146    if args.print {
147        return Ok(app.state.exit_code);
148    }
149
150    app.run(term, Duration::from_millis(100))?;
151
152    Ok(app.state.exit_code)
153}
154
155/// What git piped to us as its pager, alongside the command already found to
156/// have produced it. Reading the terminal instead would wait for input that is
157/// never coming, so that is refused outright.
158fn read_piped_input(git_argv: Option<Vec<String>>, limit: u64) -> Res<Paged> {
159    if io::stdin().is_terminal() {
160        return Err(Error::PagerWithoutInput);
161    }
162
163    let input = Capped::read(io::stdin(), limit).map_err(Error::ReadPipedInput)?;
164    Ok(Paged {
165        text: input.text,
166        truncated: input.truncated,
167        git_argv,
168    })
169}
170
171fn open_repo(dir: &Path) -> Res<Repository> {
172    log::debug!("Opening repo");
173    let repo = open_repo_from_env()?;
174    repo.set_workdir(dir, false).map_err(Error::OpenRepo)?;
175    Ok(repo)
176}
177
178fn find_git_dir() -> Res<PathBuf> {
179    log::debug!("Finding git dir");
180    let dir = PathBuf::from(
181        String::from_utf8(
182            Command::new("git")
183                .args(["rev-parse", "--show-toplevel"])
184                .output()
185                .map_err(Error::FindGitDir)?
186                .stdout,
187        )
188        .map_err(Error::GitDirUtf8)?
189        .trim_end(),
190    );
191    Ok(dir)
192}
193
194fn open_repo_from_env() -> Res<Repository> {
195    match Repository::open_from_env() {
196        Ok(repo) => Ok(repo),
197        Err(err) => Err(Error::OpenRepo(err)),
198    }
199}
200
201fn keys_to_events(keys: &[(KeyModifiers, KeyCode)]) -> Vec<Event> {
202    keys.iter()
203        .map(|(mods, key)| {
204            Event::Key(KeyEvent {
205                code: *key,
206                modifiers: *mods,
207                kind: event::KeyEventKind::Press,
208                state: KeyEventState::NONE,
209            })
210        })
211        .collect::<Vec<_>>()
212}