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 config,
115 true,
116 piped,
117 )?;
118
119 if let Some(keys_string) = &args.keys {
120 let ("", keys) = key_parser::parse_keys(keys_string).expect("Couldn't parse keys") else {
121 panic!("Couldn't parse keys");
122 };
123
124 for event in keys_to_events(&keys) {
125 app.handle_event(term, event)?;
126 }
127 }
128
129 app.redraw_now(term)?;
130
131 if args.print {
132 return Ok(app.state.exit_code);
133 }
134
135 app.run(term, Duration::from_millis(100))?;
136
137 Ok(app.state.exit_code)
138}
139
140/// What git piped to us as its pager, alongside the command already found to
141/// have produced it. Reading the terminal instead would wait for input that is
142/// never coming, so that is refused outright.
143fn read_piped_input(git_argv: Option<Vec<String>>, limit: u64) -> Res<Paged> {
144 if io::stdin().is_terminal() {
145 return Err(Error::PagerWithoutInput);
146 }
147
148 let input = Capped::read(io::stdin(), limit).map_err(Error::ReadPipedInput)?;
149 Ok(Paged {
150 text: input.text,
151 truncated: input.truncated,
152 git_argv,
153 })
154}
155
156fn open_repo(dir: &Path) -> Res<Repository> {
157 log::debug!("Opening repo");
158 let repo = open_repo_from_env()?;
159 repo.set_workdir(dir, false).map_err(Error::OpenRepo)?;
160 Ok(repo)
161}
162
163fn find_git_dir() -> Res<PathBuf> {
164 log::debug!("Finding git dir");
165 let dir = PathBuf::from(
166 String::from_utf8(
167 Command::new("git")
168 .args(["rev-parse", "--show-toplevel"])
169 .output()
170 .map_err(Error::FindGitDir)?
171 .stdout,
172 )
173 .map_err(Error::GitDirUtf8)?
174 .trim_end(),
175 );
176 Ok(dir)
177}
178
179fn open_repo_from_env() -> Res<Repository> {
180 match Repository::open_from_env() {
181 Ok(repo) => Ok(repo),
182 Err(err) => Err(Error::OpenRepo(err)),
183 }
184}
185
186fn keys_to_events(keys: &[(KeyModifiers, KeyCode)]) -> Vec<Event> {
187 keys.iter()
188 .map(|(mods, key)| {
189 Event::Key(KeyEvent {
190 code: *key,
191 modifiers: *mods,
192 kind: event::KeyEventKind::Press,
193 state: KeyEventState::NONE,
194 })
195 })
196 .collect::<Vec<_>>()
197}