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