1use std::borrow::Cow;
2use std::cell::RefCell;
3use std::collections::BTreeMap;
4use std::ffi::OsStr;
5use std::io::Read;
6use std::io::Write;
7use std::ops::DerefMut;
8use std::process::Child;
9use std::process::Command;
10use std::process::Stdio;
11use std::rc::Rc;
12use std::sync::Arc;
13use std::sync::RwLock;
14use std::thread::JoinHandle;
15use std::time::{Duration, Instant};
16
17use arboard::Clipboard;
18use crossterm::event;
19use crossterm::event::Event;
20use crossterm::event::KeyCode;
21use crossterm::event::KeyModifiers;
22use crossterm::event::MouseButton;
23use crossterm::event::MouseEvent;
24use crossterm::event::MouseEventKind;
25use git2::Repository;
26
27use crate::cli;
28use crate::cmd_log::CmdLog;
29use crate::cmd_log::CmdLogEntry;
30use crate::config::Config;
31use crate::error::Error;
32use crate::file_watcher::FileWatcher;
33use crate::item_data::Rev;
34use crate::items::RenderParams;
35use crate::menu::Menu;
36use crate::menu::PendingMenu;
37use crate::ops::Op;
38use crate::picker::PickerData;
39use crate::picker::PickerState;
40use crate::picker::{Picked, PickerStatus};
41use crate::prompt;
42use crate::rebase_todo::RebaseTodo;
43use crate::screen;
44use crate::screen::Screen;
45use crate::screen::pager::Paged;
46use crate::term::Term;
47use crate::text_input;
48use crate::ui;
49
50use super::Res;
51
52pub(crate) struct State {
53 pub repo: Rc<Repository>,
54 pub config: Arc<Config>,
55 pub pending_keys: Vec<(KeyModifiers, KeyCode)>,
56 pub quit: bool,
57 pub exit_code: i32,
61 pub screens: Vec<Screen>,
62 pub pending_menu: Option<PendingMenu>,
63 pending_cmd: Option<PendingCmd>,
64 enable_async_cmds: bool,
65 pub debug: bool,
66 pub current_cmd_log: CmdLog,
67 pub prompt: prompt::Prompt,
68 pub picker: Option<PickerState>,
69 pub picked_commit: Option<String>,
71 pub features: Rc<[String]>,
74 pub theme: Option<String>,
76 pub context: Option<Rc<str>>,
79 pub feature_keys: BTreeMap<String, char>,
81 pub clipboard: Option<Clipboard>,
82 pub read_only: bool,
85 needs_redraw: bool,
86 pub(crate) terminal_width: Option<(u16, Instant)>,
90 file_watcher: Option<FileWatcher>,
91 inhibit_close_menu: bool,
92}
93
94pub struct App {
95 pub(crate) state: State,
96}
97
98impl App {
99 pub fn create(
100 repo: Rc<Repository>,
101 size: (u16, u16),
102 args: &cli::Args,
103 config: Arc<Config>,
104 enable_async_cmds: bool,
105 piped: Option<Paged>,
106 ) -> Res<Self> {
107 let params = RenderParams {
108 size,
109 features: overlay(&[], config.general.diff_renderer.theme.as_deref()),
110 context: None,
111 streaming: enable_async_cmds,
112 };
113 let screens = match args.command {
114 _ if let Some(paged) = piped => {
116 vec![screen::pager::create(
117 Arc::clone(&config),
118 Rc::clone(&repo),
119 params,
120 paged,
121 )?]
122 }
123 Some(cli::Commands::Show { ref reference }) => {
124 vec![screen::pager::show(
125 Arc::clone(&config),
126 Rc::clone(&repo),
127 params.clone(),
128 reference,
129 )?]
130 }
131 Some(cli::Commands::Blame { ref rev, ref file }) => {
132 vec![screen::blame::create(
133 Arc::clone(&config),
134 Rc::clone(&repo),
135 params.clone(),
136 file.clone(),
137 rev.clone(),
138 None,
139 )?]
140 }
141 Some(cli::Commands::Rebase { ref upstream }) => {
142 let todo = RebaseTodo::capture(&repo, OsStr::new(upstream), &[])?;
143 vec![rebase_todo_screen(&config, &repo, params.clone(), todo)?]
144 }
145 Some(cli::Commands::SequenceEditor { ref file }) => {
146 let todo = RebaseTodo::read(&repo, file)?;
147 vec![rebase_todo_screen(&config, &repo, params.clone(), todo)?]
148 }
149 None => vec![screen::status::create(
150 Arc::clone(&config),
151 Rc::clone(&repo),
152 params,
153 )?],
154 };
155
156 let clipboard = Clipboard::new()
157 .inspect_err(|e| log::warn!("Couldn't initialize clipboard: {e}"))
158 .ok();
159 let read_only = config.general.read_only.is_on(&repo);
160 let prompt = prompt::Prompt::with_history(repo.path().join("gitu"), read_only);
161
162 let mut app = Self {
163 state: State {
164 repo,
165 feature_keys: config.general.diff_renderer.feature_keys.clone(),
166 theme: config.general.diff_renderer.theme.clone(),
167 config,
168 pending_keys: vec![],
169 enable_async_cmds,
170 debug: args.debug,
171 quit: false,
172 exit_code: match args.command {
173 Some(cli::Commands::SequenceEditor { .. }) => 1,
174 _ => 0,
175 },
176 screens,
177 pending_cmd: None,
178 pending_menu: None,
179 current_cmd_log: CmdLog::new(),
180 prompt,
181 picker: None,
182 picked_commit: None,
183 features: Rc::from([]),
184 context: None,
185 clipboard,
186 read_only,
187 file_watcher: None,
188 needs_redraw: true,
189 terminal_width: None,
190 inhibit_close_menu: false,
191 },
192 };
193
194 app.close_menu();
197 app.state.file_watcher = app.init_file_watcher()?;
198 Ok(app)
199 }
200
201 fn init_file_watcher(&mut self) -> Res<Option<FileWatcher>> {
202 if !self.state.config.general.refresh_on_file_change.enabled {
203 return Ok(None);
204 }
205
206 let hiding_untracked_files = !self
207 .state
208 .repo
209 .config()
210 .map_err(Error::ReadGitConfig)?
211 .get_bool("status.showUntrackedFiles")
212 .unwrap_or(true);
213
214 if hiding_untracked_files {
215 self.display_info("File watcher disabled (status.showUntrackedFiles is off)");
216
217 return Ok(None);
218 }
219
220 Ok(
221 FileWatcher::new(self.state.repo.workdir().expect("Bare repos unhandled"))
222 .inspect_err(|err| {
223 self.display_error(err.to_string());
224 self.display_info("File watcher disabled");
225 })
226 .ok(),
227 )
228 }
229
230 pub fn run(&mut self, term: &mut Term, max_tick_delay: Duration) -> Res<()> {
231 while !self.state.quit {
232 let event = if term.poll_event(self.poll_timeout(max_tick_delay, Instant::now()))? {
233 Some(term.read_event()?)
234 } else if term.hung_up() {
235 return Ok(());
236 } else {
237 None
238 };
239
240 self.update(term, event)?;
241 }
242
243 Ok(())
244 }
245
246 pub fn update(&mut self, term: &mut Term, event: Option<Event>) -> Res<()> {
247 if let Some(e) = event {
248 self.handle_event(term, e)?;
249 }
250
251 self.rerender_at_terminal_width(term)?;
252
253 if let Some(file_watcher) = &mut self.state.file_watcher
254 && file_watcher.pending_updates()
255 {
256 self.update_screens()?;
257 }
258
259 let finished = self.finish_screens();
260 self.handle_result(finished)?;
261
262 let handle_pending_cmd_result = self.handle_pending_cmd();
263 self.handle_result(handle_pending_cmd_result)?;
264
265 if self.state.needs_redraw {
266 self.redraw_now(term)?;
267 }
268
269 Ok(())
270 }
271
272 fn finish_screens(&mut self) -> Res<()> {
275 let mut finished = false;
276 for screen in &mut self.state.screens {
277 finished |= screen.finish()?;
278 }
279 if finished {
280 self.stage_redraw();
281 }
282 Ok(())
283 }
284
285 pub(crate) fn poll_timeout(&self, max_tick_delay: Duration, now: Instant) -> Duration {
288 match self.resize_render_due() {
289 Some(due) => max_tick_delay.min(due.saturating_duration_since(now)),
290 None => max_tick_delay,
291 }
292 }
293
294 fn resize_render_due(&self) -> Option<Instant> {
298 let (width, since) = self.state.terminal_width?;
299 let stale = self
300 .state
301 .screens
302 .iter()
303 .any(|screen| screen.rendered_width != width);
304 let delay = if self.state.enable_async_cmds {
305 RESIZE_RENDER_DELAY
306 } else {
307 Duration::ZERO
308 };
309 stale.then_some(since + delay)
310 }
311
312 fn rerender_at_terminal_width(&mut self, term: &mut Term) -> Res<()> {
316 let width = term.size().map_err(Error::Term)?.0;
317 let now = Instant::now();
318 if self
319 .state
320 .terminal_width
321 .is_none_or(|(seen, _)| seen != width)
322 {
323 self.state.terminal_width = Some((width, now));
324 }
325 match self.resize_render_due() {
326 Some(due) if now >= due && !term.has_pending_event()? => {}
327 _ => return Ok(()),
328 }
329
330 for screen in &mut self.state.screens {
331 let height = screen.size.1;
332 screen.resize(width, height)?;
333 }
334 self.rerender_screens()
335 }
336
337 pub fn update_screens(&mut self) -> Res<()> {
338 self.rebuild_screens(Screen::refresh)
339 }
340
341 pub fn rerender_screens(&mut self) -> Res<()> {
346 self.rebuild_screens(Screen::refresh_keeping_position)
347 }
348
349 pub(crate) fn rerender_screen(&mut self) -> Res<()> {
352 let features = self.overlay();
353 let screen = self.state.screens.last_mut().unwrap();
354 screen.features = features;
355 screen.refresh_keeping_position()?;
356 self.stage_redraw();
357 Ok(())
358 }
359
360 fn overlay(&self) -> Rc<[String]> {
362 overlay(&self.state.features, self.state.theme.as_deref())
363 }
364
365 fn rebuild_screens(&mut self, rebuild: impl Fn(&mut Screen) -> Res<()>) -> Res<()> {
366 let features = self.overlay();
367 for screen in &mut self.state.screens {
368 screen.features = Rc::clone(&features);
369 screen.context = self.state.context.clone();
370 rebuild(screen)?;
371 }
372
373 self.stage_redraw();
374 Ok(())
375 }
376
377 pub fn handle_event(&mut self, term: &mut Term, event: Event) -> Res<()> {
378 match event {
379 Event::Resize(w, h) => {
380 for screen in self.state.screens.iter_mut() {
384 screen.resize(w, h)?;
385 }
386
387 self.stage_redraw();
388 Ok(())
389 }
390 Event::Mouse(ref mouse) => {
391 if self.state.config.general.mouse_support
392 && self.handle_mouse_input(term, mouse)?
393 {
394 self.stage_redraw();
395 }
396 Ok(())
397 }
398 Event::Key(mut key) => {
399 if self.state.pending_cmd.is_none() {
400 self.state.current_cmd_log.clear();
401 }
402
403 if self.state.pending_cmd.is_none() {
404 self.state.current_cmd_log.clear();
405 }
406
407 if matches!(key.code, KeyCode::Char(_) | KeyCode::BackTab) {
412 key.modifiers = key.modifiers.difference(KeyModifiers::SHIFT);
413 }
414
415 if self.state.picker.is_some() {
416 self.handle_picker_input(key)?;
417 } else if self.state.prompt.state.focused {
418 self.state.prompt.handle_key(key);
419 } else {
420 self.handle_key_input(term, key)?;
421 }
422
423 self.stage_redraw();
424 Ok(())
425 }
426 _ => Ok(()),
427 }
428 }
429
430 pub fn redraw_now(&mut self, term: &mut Term) -> Res<()> {
431 if self.state.screens.last_mut().is_some() {
432 ui::ui(term, &mut self.state)?;
433 self.state.needs_redraw = false;
434 };
435
436 Ok(())
437 }
438
439 pub fn print_if_one_screen(&mut self, term: &mut Term) -> Res<bool> {
443 let screen = self.state.screens.last_mut().unwrap();
444 let (_, rows) = term.size().map_err(Error::Term)?;
445 let rows = rows.saturating_sub(1).into();
446 loop {
447 if !screen.fits_in(rows) {
448 return Ok(false);
449 }
450 if !screen.is_drawing() {
451 break;
452 }
453 if !screen.finish()? {
454 std::thread::sleep(Duration::from_millis(10));
455 }
456 }
457
458 screen::print_screen(term, screen)?;
459 Ok(true)
460 }
461
462 pub fn stage_redraw(&mut self) {
463 self.state.needs_redraw = true;
464 }
465
466 fn handle_key_input(&mut self, term: &mut Term, key: event::KeyEvent) -> Res<()> {
467 if key.kind != event::KeyEventKind::Press {
468 return Ok(());
469 }
470
471 let menu = match &self.state.pending_menu {
472 None => self.base_menu().unwrap_or(Menu::Root),
473 Some(menu) if menu.menu == Menu::Help => Menu::Root,
474 Some(menu) => menu.menu,
475 };
476
477 self.state.pending_keys.push((key.modifiers, key.code));
478
479 let matching_bindings = self
480 .state
481 .config
482 .bindings
483 .match_bindings(&menu, &self.state.pending_keys)
484 .collect::<Vec<_>>();
485
486 match matching_bindings[..] {
487 [binding] => {
488 if binding.keys == self.state.pending_keys {
489 let op = binding.op.clone();
490 self.state.pending_keys.clear();
491 self.handle_op(op, term)?;
492 }
493 }
494 [] => {
495 let feature = match self.state.pending_keys[..] {
496 [(KeyModifiers::NONE, KeyCode::Char(key))] if menu == Menu::Root => {
497 self.feature_with_key(key)
498 }
499 _ => None,
500 };
501 self.state.pending_keys.clear();
502 if let Some(name) = feature {
503 if self.state.config.general.commands.contains_key(&name) {
504 crate::ops::command::run(self, term, &name)?;
505 } else if self.state.config.general.views.contains_key(&name) {
506 crate::ops::command::open_view(self, term, &name)?;
507 } else {
508 crate::ops::editor::toggle_feature(self, &name)?;
509 }
510 }
511 }
512 [_, ..] => (),
513 }
514
515 Ok(())
516 }
517
518 fn handle_mouse_input(&mut self, term: &mut Term, mouse: &MouseEvent) -> Res<bool> {
519 match mouse.kind {
520 MouseEventKind::Down(MouseButton::Left) => {
521 let click_y = mouse.row as usize;
522 if self.screen().is_valid_screen_line(click_y) {
523 let old_selected_item_id = self.screen().get_selected_item().id;
524 self.handle_op(Op::MoveToScreenLine(click_y), term)?;
525 let new_selected_item = self.screen().get_selected_item();
526
527 if old_selected_item_id == new_selected_item.id {
528 if new_selected_item.data.is_section() {
531 self.handle_op(Op::ToggleSection, term)?;
532 } else {
533 self.handle_op(Op::Show, term)?;
534 }
535 }
536 }
537 }
538 MouseEventKind::Down(MouseButton::Right) => {
539 let click_y = mouse.row as usize;
540 if self.screen().is_valid_screen_line(click_y) {
541 self.handle_op(Op::MoveToScreenLine(click_y), term)?;
542 self.handle_op(Op::Show, term)?;
543 }
544 }
545 MouseEventKind::ScrollUp => {
546 let scroll_lines = self.state.config.general.mouse_scroll_lines;
547 if scroll_lines > 0 {
548 self.screen_mut().scroll_view_up(scroll_lines);
549 }
550 }
551 MouseEventKind::ScrollDown => {
552 let scroll_lines = self.state.config.general.mouse_scroll_lines;
553 if scroll_lines > 0 {
554 self.screen_mut().scroll_view_down(scroll_lines);
555 }
556 }
557 _ => return Ok(false),
558 }
559
560 Ok(true)
561 }
562
563 pub(crate) fn handle_op(&mut self, op: Op, term: &mut Term) -> Res<()> {
564 if self.state.read_only && op.writes() {
565 return Ok(());
566 }
567 let item_data = self.screen().selected_target();
568 let implementation = op.clone().implementation();
569
570 if let Some(mut action) = implementation.get_action(&item_data) {
571 let result = Rc::get_mut(&mut action).unwrap()(self, term);
572 self.handle_result(result)?;
573 if !self.state.inhibit_close_menu {
574 self.close_menu();
575 }
576 self.state.inhibit_close_menu = false;
577 } else if matches!(op, Op::Show | Op::ShowOrUnfold) {
578 let hint = self.screen().unmarked_commits_hint();
579 self.display_error(match hint {
580 Some(hint) => format!("No commit to open: {hint}"),
581 None => "Nothing to open here".to_owned(),
582 });
583 }
584
585 Ok(())
586 }
587
588 fn handle_result<T>(&mut self, result: Res<T>) -> Res<()> {
589 match result {
590 Ok(_) => Ok(()),
591 Err(Error::NoMoreEvents) => Err(Error::NoMoreEvents),
592 Err(Error::PromptAborted) => Ok(()),
593 Err(error) => {
594 self.state
595 .current_cmd_log
596 .push(CmdLogEntry::Error(error.to_string()));
597
598 Ok(())
599 }
600 }
601 }
602
603 pub(crate) fn render_params(&self, size: (u16, u16)) -> RenderParams {
606 RenderParams {
607 size,
608 features: self.overlay(),
609 context: self.state.context.clone(),
610 streaming: self.state.enable_async_cmds,
611 }
612 }
613
614 pub fn close_menu(&mut self) {
615 let hide = self.screen().menu.is_some() && !self.screen().show_menu;
616 self.state.pending_menu = self.base_menu().map(|menu| PendingMenu {
617 is_hidden: hide,
618 ..PendingMenu::init(menu)
619 });
620 }
621
622 pub(crate) fn base_menu(&self) -> Option<Menu> {
625 self.screen().menu.or_else(|| root_menu(&self.state.config))
626 }
627
628 pub fn inhibit_close_menu(&mut self) {
629 self.state.inhibit_close_menu = true;
630 }
631
632 pub(crate) fn screen_mut(&mut self) -> &mut Screen {
633 self.state.screens.last_mut().expect("No screen")
634 }
635
636 pub(crate) fn screen(&self) -> &Screen {
637 self.state.screens.last().expect("No screen")
638 }
639
640 pub fn display_info<S: Into<Cow<'static, str>>>(&mut self, message: S) {
642 self.state
643 .current_cmd_log
644 .push(CmdLogEntry::Info(message.into().into_owned()));
645 }
646
647 pub fn display_error<S: Into<Cow<'static, str>>>(&mut self, message: S) {
649 self.state
650 .current_cmd_log
651 .push(CmdLogEntry::Error(message.into().into_owned()));
652 }
653
654 pub fn run_cmd(&mut self, term: &mut Term, input: &[u8], cmd: Command) -> Res<()> {
657 self.run_cmd_async(term, input, cmd)?;
658 self.await_pending_cmd()?;
659 self.handle_pending_cmd()?;
660 Ok(())
661 }
662
663 pub fn run_cmd_async(&mut self, term: &mut Term, input: &[u8], cmd: Command) -> Res<()> {
666 let shown = crate::cmd_log::command_args(&cmd);
667 self.run_cmd_async_shown(term, input, cmd, shown)
668 }
669
670 pub(crate) fn run_shell_async(
673 &mut self,
674 term: &mut Term,
675 command: &str,
676 env: Vec<(&'static str, String)>,
677 ) -> Res<()> {
678 let mut cmd = Command::new("sh");
679 cmd.args(["-c", command]).envs(env);
680 self.run_cmd_async_shown(term, &[], cmd, command.to_owned().into())
681 }
682
683 fn run_cmd_async_shown(
684 &mut self,
685 term: &mut Term,
686 input: &[u8],
687 mut cmd: Command,
688 shown: Cow<'static, str>,
689 ) -> Res<()> {
690 cmd.env("CLICOLOR_FORCE", "1"); if self.state.pending_cmd.is_some() {
693 return Err(Error::CmdAlreadyRunning);
694 }
695
696 cmd.current_dir(self.state.repo.workdir().expect("No workdir"));
697
698 cmd.stdin(Stdio::piped());
699 cmd.stdout(Stdio::piped());
700 cmd.stderr(Stdio::piped());
701
702 let log_entry = self.state.current_cmd_log.push_cmd(shown);
703
704 ui::ui(term, &mut self.state)?;
705
706 let mut child = cmd.spawn().map_err(Error::SpawnCmd)?;
707
708 let mut stdin = child.stdin.take().unwrap();
711 let input = input.to_vec();
712 let stdin = std::thread::spawn(move || stdin.write_all(&input));
713 let stdout = read_to_end_on_thread(child.stdout.take().unwrap());
714 let stderr = read_to_end_on_thread(child.stderr.take().unwrap());
715
716 self.state.pending_cmd = Some(PendingCmd {
717 child,
718 log_entry,
719 stdin,
720 stdout,
721 stderr,
722 });
723
724 if !self.state.enable_async_cmds {
725 self.await_pending_cmd()?;
726 }
727
728 Ok(())
729 }
730
731 fn await_pending_cmd(&mut self) -> Res<()> {
732 if let Some(PendingCmd { child, .. }) = &mut self.state.pending_cmd {
733 child.wait().map_err(Error::CouldntAwaitCmd)?;
734 }
735 Ok(())
736 }
737
738 fn handle_pending_cmd(&mut self) -> Res<()> {
740 let Some(PendingCmd { ref mut child, .. }) = self.state.pending_cmd else {
741 return Ok(());
742 };
743
744 let Some(status) = child.try_wait().map_err(Error::CouldntAwaitCmd)? else {
745 return Ok(());
746 };
747
748 log::debug!("pending cmd finished with {status:?}");
749
750 let pending = self.state.pending_cmd.take().unwrap();
751 let result = write_child_output_to_log(pending, status);
752 self.update_screens()?;
753 result?;
754
755 Ok(())
756 }
757
758 pub fn run_cmd_interactive(&mut self, term: &mut Term, mut cmd: Command) -> Res<()> {
759 cmd.env("CLICOLOR_FORCE", "1"); if self.state.pending_cmd.is_some() {
762 return Err(Error::CmdAlreadyRunning);
763 }
764
765 cmd.current_dir(self.state.repo.workdir().ok_or(Error::NoRepoWorkdir)?);
766
767 self.state
768 .current_cmd_log
769 .push_cmd_with_output(&cmd, "\n".into());
770 self.redraw_now(term)?;
771
772 eprint!("\r");
773
774 cmd.stderr(Stdio::piped());
776
777 term.reset_term_stay_on_alt_screeen(&self.state.config)
778 .map_err(Error::Term)?;
779
780 let mut child = cmd.spawn().map_err(Error::SpawnCmd)?;
781
782 drop(child.stdin.take());
784
785 let mut stderr = Vec::new();
786
787 tee(
788 child.stderr.as_mut(),
789 &mut [&mut std::io::stderr(), &mut stderr],
790 )
791 .map_err(Error::Term)?;
792
793 let status = child.wait().map_err(Error::CouldntAwaitCmd)?;
794
795 term.setup_term(&self.state.config).map_err(Error::Term)?;
796
797 let out_utf8 = String::from_utf8(strip_ansi_escapes::strip(stderr.clone()))
798 .expect("Error turning command output to String")
799 .into();
800
801 self.state.current_cmd_log.clear();
802 self.state
803 .current_cmd_log
804 .push_cmd_with_output(&cmd, out_utf8);
805
806 term.clear().map_err(Error::Term)?;
807 self.update_screens()?;
808
809 if !status.success() {
810 return Err(Error::CmdBadExit(
812 crate::cmd_log::command_args(&cmd).into_owned(),
813 status.code(),
814 String::new(),
815 ));
816 }
817
818 Ok(())
819 }
820
821 pub fn hide_menu(&mut self) {
822 if let Some(ref mut menu) = self.state.pending_menu {
823 menu.is_hidden = true;
824 }
825 }
826
827 fn unhide_menu(&mut self) {
828 if let Some(ref mut menu) = self.state.pending_menu {
829 menu.is_hidden = false;
830 }
831 }
832
833 pub(crate) fn selected_rev(&self) -> Option<Rev> {
834 self.screen().get_selected_item().data.rev()
835 }
836
837 pub(crate) fn prompt(&mut self, term: &mut Term, params: &PromptParams) -> Res<String> {
838 let default = (params.create_default_value)(self);
839
840 let prompt_text = match (params.prompt, &default) {
845 ("", _) => Cow::Borrowed(""),
846 (prompt, _) if params.bare => Cow::Borrowed(prompt),
847 (prompt, Some(default)) if !params.prefill => format!("{prompt} ({default}):").into(),
848 (prompt, _) => format!("{prompt}:").into(),
849 };
850
851 if params.hide_menu {
852 self.hide_menu();
853 }
854
855 self.state.prompt.set(prompt::PromptData {
856 prompt_text,
857 bare: params.bare,
858 });
859
860 if params.prefill
861 && let Some(default) = default
862 {
863 self.state.prompt.state.value = default;
864 self.state.prompt.state.move_to(usize::MAX);
865 }
866 self.state.prompt.start_history(params.history)?;
867
868 let result = self.handle_prompt(term, params);
869
870 self.unhide_menu();
871 self.state.prompt.reset();
872
873 result
874 }
875
876 fn handle_prompt(&mut self, term: &mut Term, params: &PromptParams) -> Res<String> {
877 self.redraw_now(term)?;
878
879 loop {
880 let event = term.read_event()?;
881 let was = self.state.prompt.state.value.clone();
882 self.handle_event(term, event)?;
883
884 if self.state.prompt.state.status == text_input::Status::Done {
885 let value = get_prompt_result(params, self)?;
886 if let Some(kind) = params.history {
887 self.state.prompt.remember(kind, &value)?;
888 }
889 return Ok(value);
890 } else if self.state.prompt.state.status == text_input::Status::Aborted {
891 return Err(Error::PromptAborted);
892 }
893 if let Some(on_edit) = ¶ms.on_edit
894 && self.state.prompt.state.value != was
895 {
896 let value = self.state.prompt.state.value.clone();
897 on_edit(self, &value);
898 }
899
900 self.redraw_now(term)?;
901 }
902 }
903
904 pub fn confirm(&mut self, term: &mut Term, prompt: &'static str) -> Res<()> {
905 self.hide_menu();
906 self.state.prompt.set(prompt::PromptData {
907 prompt_text: prompt.into(),
908 bare: false,
909 });
910
911 let result = self.handle_confirm(term);
912
913 self.unhide_menu();
914 self.state.prompt.reset();
915
916 result
917 }
918
919 fn handle_confirm(&mut self, term: &mut Term) -> Res<()> {
920 self.redraw_now(term)?;
921 loop {
922 let event = term.read_event()?;
923 self.handle_event(term, event)?;
924
925 match self.state.prompt.state.value.as_str() {
926 "y" => {
927 return Ok(());
928 }
929 "" => (),
930 _ => {
931 return Err(Error::PromptAborted);
932 }
933 }
934
935 self.redraw_now(term)?;
936 }
937 }
938
939 pub fn pick(&mut self, term: &mut Term, picker_state: PickerState) -> Res<Option<PickerData>> {
969 Ok(self
970 .pick_or_set_key(term, picker_state)?
971 .map(|picked| match picked {
972 Picked::Item(data) => data,
973 Picked::KeySet(..) => unreachable!("only a picker assigning keys sets them"),
974 }))
975 }
976
977 pub(crate) fn pick_or_set_key(
979 &mut self,
980 term: &mut Term,
981 picker_state: PickerState,
982 ) -> Res<Option<Picked>> {
983 self.state.picker = Some(picker_state);
984 let result = self.handle_picker(term, &mut |_, _| Ok(()));
985
986 self.state.picker = None;
987
988 result
989 }
990
991 pub(crate) fn pick_previewing(
993 &mut self,
994 term: &mut Term,
995 picker_state: PickerState,
996 preview: &mut dyn FnMut(&mut App, &PickerData) -> Res<()>,
997 ) -> Res<Option<PickerData>> {
998 self.state.picker = Some(picker_state);
999 let result = self.handle_picker(term, preview);
1000
1001 self.state.picker = None;
1002
1003 Ok(result?.map(|picked| match picked {
1004 Picked::Item(data) => data,
1005 Picked::KeySet(..) => unreachable!("only a picker assigning keys sets them"),
1006 }))
1007 }
1008
1009 pub fn pick_commit(&mut self, term: &mut Term) -> Res<Option<String>> {
1013 let params = self.render_params(self.screen().size);
1014 let selected = match &self.screen().get_selected_item().data {
1015 crate::item_data::ItemData::Commit { oid, .. } => Some(oid.clone()),
1016 _ => None,
1017 };
1018 let params = RenderParams {
1021 streaming: params.streaming && selected.is_none(),
1022 ..params
1023 };
1024 self.state.screens.push(screen::pager::log(
1025 Arc::clone(&self.state.config),
1026 Rc::clone(&self.state.repo),
1027 params,
1028 COMMIT_PICK_LIMIT,
1029 None,
1030 None,
1031 )?);
1032 self.screen_mut().menu = Some(Menu::CommitPicker);
1033 self.close_menu();
1034
1035 if let Some(selected) = selected {
1037 self.screen_mut().select_matching(|data| {
1038 matches!(data, crate::item_data::ItemData::Commit { oid, .. } if *oid == selected)
1039 });
1040 }
1041
1042 let depth = self.state.screens.len();
1043 self.redraw_now(term)?;
1044
1045 loop {
1046 let event = term.read_event()?;
1047 self.handle_event(term, event)?;
1048
1049 if let Some(oid) = self.state.picked_commit.take() {
1050 self.state.screens.truncate(depth - 1);
1051 self.close_menu();
1052 return Ok(Some(oid));
1053 }
1054 if self.state.screens.len() < depth {
1055 return Ok(None); }
1057
1058 self.redraw_now(term)?;
1059 }
1060 }
1061
1062 fn handle_picker(
1063 &mut self,
1064 term: &mut Term,
1065 preview: &mut dyn FnMut(&mut App, &PickerData) -> Res<()>,
1066 ) -> Res<Option<Picked>> {
1067 self.redraw_now(term)?;
1068 let selected = |app: &App| {
1069 app.state
1070 .picker
1071 .as_ref()
1072 .and_then(|picker| picker.selected())
1073 .map(|item| item.data.clone())
1074 };
1075 let mut previewed = selected(self);
1076
1077 loop {
1078 let event = term.read_event()?;
1079 self.handle_event(term, event)?;
1080
1081 if let Some(ref picker) = self.state.picker {
1082 let selected = || picker.selected().map(|item| item.data.clone());
1083 match *picker.status() {
1084 PickerStatus::Done => return Ok(selected().map(Picked::Item)),
1085 PickerStatus::KeySet(key) => {
1086 return Ok(selected().map(|data| Picked::KeySet(key, data)));
1087 }
1088 PickerStatus::Cancelled => return Ok(None),
1090 PickerStatus::Active => (),
1091 }
1092 }
1093
1094 let now = selected(self);
1097 if now != previewed && !matches!(term.poll_event(Duration::ZERO), Ok(true)) {
1098 if let Some(data) = &now {
1099 preview(self, data)?;
1100 }
1101 previewed = now;
1102 }
1103
1104 self.redraw_now(term)?;
1105 }
1106 }
1107
1108 fn feature_with_key_in_one_row(&self, key: event::KeyEvent) -> Option<String> {
1111 let picker = self.state.picker.as_ref()?;
1112 let quit = &self.state.config.picker_bindings.quit;
1113 match (key.code, key.modifiers) {
1114 (event::KeyCode::Char(c), event::KeyModifiers::NONE)
1115 if picker.is_one_row()
1116 && !c.is_ascii_digit()
1117 && !quit.iter().any(|b| b == &vec![(key.modifiers, key.code)]) =>
1118 {
1119 self.feature_with_key(c).filter(|name| {
1120 let general = &self.state.config.general;
1121 !general.commands.contains_key(name) && !general.views.contains_key(name)
1122 })
1123 }
1124 _ => None,
1125 }
1126 }
1127
1128 fn feature_with_key(&self, key: char) -> Option<String> {
1131 self.state
1132 .feature_keys
1133 .iter()
1134 .find(|(_, held)| **held == key)
1135 .map(|(feature, _)| feature.clone())
1136 }
1137
1138 fn handle_picker_input(&mut self, key: event::KeyEvent) -> Res<()> {
1139 if key.kind != event::KeyEventKind::Press {
1140 return Ok(());
1141 }
1142
1143 if let Some(feature) = self.feature_with_key_in_one_row(key) {
1147 return crate::ops::editor::toggle_feature(self, &feature);
1148 }
1149
1150 if let Some(ref mut picker) = self.state.picker {
1151 let bindings = &self.state.config.picker_bindings;
1152 let key_combo = vec![(key.modifiers, key.code)];
1153 let typed = match (key.code, key.modifiers) {
1154 (
1155 event::KeyCode::Char(c),
1156 event::KeyModifiers::NONE | event::KeyModifiers::SHIFT,
1157 ) => Some(c),
1158 _ => None,
1159 };
1160
1161 if bindings.next.iter().any(|b| b == &key_combo) {
1162 picker.next();
1163 } else if bindings.previous.iter().any(|b| b == &key_combo) {
1164 picker.previous();
1165 } else if bindings.done.iter().any(|b| b == &key_combo) {
1166 picker.done();
1167 } else if bindings.cancel.iter().any(|b| b == &key_combo) {
1168 picker.cancel();
1169 } else if bindings.toggle_list.iter().any(|b| b == &key_combo) {
1170 picker.toggle_one_row();
1171 } else if picker.is_one_row() {
1172 if bindings.quit.iter().any(|b| b == &key_combo) {
1174 picker.cancel();
1175 } else if let Some(digit) = typed.and_then(|c| c.to_digit(10)) {
1176 picker.select_number(digit as usize);
1177 }
1178 } else if picker.assigns_keys() {
1179 if picker.grid().is_some() && key.code == event::KeyCode::Left {
1183 picker.move_column(-1);
1184 } else if picker.grid().is_some() && key.code == event::KeyCode::Right {
1185 picker.move_column(1);
1186 } else if bindings.quit.iter().any(|b| b == &key_combo) {
1187 picker.cancel();
1188 } else if let Some(c) = typed {
1189 picker.set_key(c);
1190 }
1191 } else {
1192 picker.input_state.handle_key_event(key);
1194 picker.update_filter();
1195 }
1196 }
1197 Ok(())
1198 }
1199}
1200
1201fn overlay(features: &[String], theme: Option<&str>) -> Rc<[String]> {
1205 features
1206 .iter()
1207 .cloned()
1208 .chain(theme.map(str::to_owned))
1209 .collect()
1210}
1211
1212fn get_prompt_result(params: &PromptParams, app: &mut App) -> Res<String> {
1213 let input = app.state.prompt.state.value.as_str();
1214 if params.prefill {
1215 return Ok(input.to_owned());
1216 }
1217
1218 let default_value = (params.create_default_value)(app);
1219
1220 let value = match (input, &default_value) {
1221 ("", None) => "",
1222 ("", Some(selected)) => selected,
1223 (value, _) => value,
1224 };
1225
1226 Ok(value.to_string())
1227}
1228
1229fn tee(maybe_input: Option<&mut impl Read>, outputs: &mut [&mut dyn Write]) -> std::io::Result<()> {
1230 let Some(input) = maybe_input else {
1231 return Ok(());
1232 };
1233
1234 let mut buf = [0u8; 1024];
1235
1236 loop {
1237 let num_read = input.read(&mut buf)?;
1238 if num_read == 0 {
1239 break;
1240 }
1241
1242 let buf = &buf[..num_read];
1243 for output in &mut *outputs {
1244 output.write_all(buf)?;
1245 }
1246 }
1247
1248 Ok(())
1249}
1250
1251fn rebase_todo_screen(
1252 config: &Arc<Config>,
1253 repo: &Rc<Repository>,
1254 params: RenderParams,
1255 todo: RebaseTodo,
1256) -> Res<Screen> {
1257 screen::rebase_todo::create(
1258 Arc::clone(config),
1259 Rc::clone(repo),
1260 params,
1261 Rc::new(RefCell::new(todo)),
1262 )
1263}
1264
1265const COMMIT_PICK_LIMIT: usize = 256;
1267
1268const RESIZE_RENDER_DELAY: Duration = Duration::from_millis(50);
1272
1273pub(crate) fn root_menu(config: &Config) -> Option<Menu> {
1274 if config.general.always_show_help.enabled {
1275 Some(Menu::Help)
1276 } else {
1277 None
1278 }
1279}
1280
1281struct PendingCmd {
1284 child: Child,
1285 log_entry: Arc<RwLock<CmdLogEntry>>,
1286 stdin: JoinHandle<std::io::Result<()>>,
1287 stdout: JoinHandle<std::io::Result<Vec<u8>>>,
1288 stderr: JoinHandle<std::io::Result<Vec<u8>>>,
1289}
1290
1291fn read_to_end_on_thread(
1292 mut pipe: impl Read + Send + 'static,
1293) -> JoinHandle<std::io::Result<Vec<u8>>> {
1294 std::thread::spawn(move || {
1295 let mut bytes = vec![];
1296 pipe.read_to_end(&mut bytes)?;
1297 Ok(bytes)
1298 })
1299}
1300
1301fn write_child_output_to_log(pending: PendingCmd, status: std::process::ExitStatus) -> Res<()> {
1302 let mut log = pending.log_entry.write().unwrap();
1303
1304 let CmdLogEntry::Cmd { args, out: out_log } = log.deref_mut() else {
1305 unreachable!("pending_cmd is always CmdLogEntry::Cmd variant");
1306 };
1307
1308 let join = |handle: JoinHandle<std::io::Result<Vec<u8>>>| {
1309 handle
1310 .join()
1311 .expect("reading a command's output panicked")
1312 .map_err(Error::CouldntReadCmdOutput)
1313 };
1314 let err_bytes = join(pending.stderr)?;
1315 let mut out_bytes = err_bytes.clone();
1316 out_bytes.extend(join(pending.stdout)?);
1317
1318 let out_string = String::from_utf8_lossy(&out_bytes).to_string();
1319 *out_log = Some(out_string.into());
1320
1321 if !status.success() {
1322 return Err(Error::CmdBadExit(
1323 args.to_string(),
1324 status.code(),
1325 String::from_utf8_lossy(&err_bytes).into_owned(),
1326 ));
1327 }
1328
1329 pending
1330 .stdin
1331 .join()
1332 .expect("writing a command's input panicked")
1333 .map_err(Error::Term)
1334}
1335
1336type DefaultFn = Box<dyn Fn(&App) -> Option<String>>;
1337type EditFn = Box<dyn Fn(&mut App, &str)>;
1338
1339pub(crate) struct PromptParams {
1340 pub prompt: &'static str,
1341 pub create_default_value: DefaultFn,
1342 pub hide_menu: bool,
1343 pub prefill: bool,
1346 pub history: Option<prompt::HistoryKind>,
1347 pub bare: bool,
1350 pub on_edit: Option<EditFn>,
1352}
1353
1354impl Default for PromptParams {
1355 fn default() -> Self {
1356 Self {
1357 prompt: "",
1358 create_default_value: Box::new(|_| None),
1359 hide_menu: true,
1360 prefill: false,
1361 history: None,
1362 bare: false,
1363 on_edit: None,
1364 }
1365 }
1366}