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::time::Duration;
15
16use arboard::Clipboard;
17use crossterm::event;
18use crossterm::event::Event;
19use crossterm::event::KeyCode;
20use crossterm::event::KeyModifiers;
21use crossterm::event::MouseButton;
22use crossterm::event::MouseEvent;
23use crossterm::event::MouseEventKind;
24use git2::Repository;
25
26use crate::cli;
27use crate::cmd_log::CmdLog;
28use crate::cmd_log::CmdLogEntry;
29use crate::config::Config;
30use crate::error::Error;
31use crate::file_watcher::FileWatcher;
32use crate::item_data::Rev;
33use crate::items::RenderParams;
34use crate::menu::Menu;
35use crate::menu::PendingMenu;
36use crate::ops::Op;
37use crate::picker::PickerData;
38use crate::picker::PickerState;
39use crate::picker::{Picked, PickerStatus};
40use crate::prompt;
41use crate::rebase_todo::RebaseTodo;
42use crate::screen;
43use crate::screen::Screen;
44use crate::screen::pager::Paged;
45use crate::term::Term;
46use crate::text_input;
47use crate::ui;
48
49use super::Res;
50
51pub(crate) struct State {
52 pub repo: Rc<Repository>,
53 pub config: Arc<Config>,
54 pub pending_keys: Vec<(KeyModifiers, KeyCode)>,
55 pub quit: bool,
56 pub exit_code: i32,
60 pub screens: Vec<Screen>,
61 pub pending_menu: Option<PendingMenu>,
62 pending_cmd: Option<(Child, Arc<RwLock<CmdLogEntry>>)>,
63 enable_async_cmds: bool,
64 pub debug: bool,
65 pub current_cmd_log: CmdLog,
66 pub prompt: prompt::Prompt,
67 pub picker: Option<PickerState>,
68 pub picked_commit: Option<String>,
70 pub features: Rc<[String]>,
73 pub context: Option<Rc<str>>,
76 pub feature_keys: BTreeMap<String, char>,
78 pub clipboard: Option<Clipboard>,
79 needs_redraw: bool,
80 file_watcher: Option<FileWatcher>,
81 inhibit_close_menu: bool,
82}
83
84pub struct App {
85 pub(crate) state: State,
86}
87
88impl App {
89 pub fn create(
90 repo: Rc<Repository>,
91 size: (u16, u16),
92 args: &cli::Args,
93 config: Arc<Config>,
94 enable_async_cmds: bool,
95 piped: Option<Paged>,
96 ) -> Res<Self> {
97 let params = RenderParams {
98 size,
99 features: Rc::from([]),
100 context: None,
101 streaming: enable_async_cmds,
102 };
103 let screens = match args.command {
104 _ if let Some(paged) = piped => {
106 vec![screen::pager::create(
107 Arc::clone(&config),
108 Rc::clone(&repo),
109 params,
110 paged,
111 )?]
112 }
113 Some(cli::Commands::Show { ref reference }) => {
114 vec![screen::pager::show(
115 Arc::clone(&config),
116 Rc::clone(&repo),
117 params.clone(),
118 reference,
119 )?]
120 }
121 Some(cli::Commands::Blame { ref rev, ref file }) => {
122 vec![screen::blame::create(
123 Arc::clone(&config),
124 Rc::clone(&repo),
125 params.clone(),
126 file.clone(),
127 rev.clone(),
128 None,
129 )?]
130 }
131 Some(cli::Commands::Rebase { ref upstream }) => {
132 let todo = RebaseTodo::capture(&repo, OsStr::new(upstream), &[])?;
133 vec![rebase_todo_screen(&config, &repo, params.clone(), todo)?]
134 }
135 Some(cli::Commands::SequenceEditor { ref file }) => {
136 let todo = RebaseTodo::read(&repo, file)?;
137 vec![rebase_todo_screen(&config, &repo, params.clone(), todo)?]
138 }
139 None => vec![screen::status::create(
140 Arc::clone(&config),
141 Rc::clone(&repo),
142 params,
143 )?],
144 };
145
146 let clipboard = Clipboard::new()
147 .inspect_err(|e| log::warn!("Couldn't initialize clipboard: {e}"))
148 .ok();
149 let prompt = prompt::Prompt::with_history(repo.path().join("gitu"));
150
151 let mut app = Self {
152 state: State {
153 repo,
154 feature_keys: config.general.diff_renderer.feature_keys.clone(),
155 config,
156 pending_keys: vec![],
157 enable_async_cmds,
158 debug: args.debug,
159 quit: false,
160 exit_code: match args.command {
161 Some(cli::Commands::SequenceEditor { .. }) => 1,
162 _ => 0,
163 },
164 screens,
165 pending_cmd: None,
166 pending_menu: None,
167 current_cmd_log: CmdLog::new(),
168 prompt,
169 picker: None,
170 picked_commit: None,
171 features: Rc::from([]),
172 context: None,
173 clipboard,
174 file_watcher: None,
175 needs_redraw: true,
176 inhibit_close_menu: false,
177 },
178 };
179
180 app.close_menu();
183 app.state.file_watcher = app.init_file_watcher()?;
184 Ok(app)
185 }
186
187 fn init_file_watcher(&mut self) -> Res<Option<FileWatcher>> {
188 if !self.state.config.general.refresh_on_file_change.enabled {
189 return Ok(None);
190 }
191
192 let hiding_untracked_files = !self
193 .state
194 .repo
195 .config()
196 .map_err(Error::ReadGitConfig)?
197 .get_bool("status.showUntrackedFiles")
198 .unwrap_or(true);
199
200 if hiding_untracked_files {
201 self.display_info("File watcher disabled (status.showUntrackedFiles is off)");
202
203 return Ok(None);
204 }
205
206 Ok(
207 FileWatcher::new(self.state.repo.workdir().expect("Bare repos unhandled"))
208 .inspect_err(|err| {
209 self.display_error(err.to_string());
210 self.display_info("File watcher disabled");
211 })
212 .ok(),
213 )
214 }
215
216 pub fn run(&mut self, term: &mut Term, max_tick_delay: Duration) -> Res<()> {
217 while !self.state.quit {
218 let event = if term.poll_event(max_tick_delay)? {
219 Some(term.read_event()?)
220 } else if term.hung_up() {
221 return Ok(());
222 } else {
223 None
224 };
225
226 self.update(term, event)?;
227 }
228
229 Ok(())
230 }
231
232 pub fn update(&mut self, term: &mut Term, event: Option<Event>) -> Res<()> {
233 if let Some(e) = event {
234 self.handle_event(term, e)?;
235 }
236
237 if let Some(file_watcher) = &mut self.state.file_watcher
238 && file_watcher.pending_updates()
239 {
240 self.update_screens()?;
241 }
242
243 let finished = self.finish_screens();
244 self.handle_result(finished)?;
245
246 let handle_pending_cmd_result = self.handle_pending_cmd();
247 self.handle_result(handle_pending_cmd_result)?;
248
249 if self.state.needs_redraw {
250 self.redraw_now(term)?;
251 }
252
253 Ok(())
254 }
255
256 fn finish_screens(&mut self) -> Res<()> {
259 let mut finished = false;
260 for screen in &mut self.state.screens {
261 finished |= screen.finish()?;
262 }
263 if finished {
264 self.stage_redraw();
265 }
266 Ok(())
267 }
268
269 pub fn update_screens(&mut self) -> Res<()> {
270 self.rebuild_screens(Screen::refresh)
271 }
272
273 pub fn rerender_screens(&mut self) -> Res<()> {
278 self.rebuild_screens(Screen::refresh_keeping_position)
279 }
280
281 fn rebuild_screens(&mut self, rebuild: impl Fn(&mut Screen) -> Res<()>) -> Res<()> {
282 let features = Rc::clone(&self.state.features);
283 for screen in &mut self.state.screens {
284 screen.features = Rc::clone(&features);
285 screen.context = self.state.context.clone();
286 rebuild(screen)?;
287 }
288
289 self.stage_redraw();
290 Ok(())
291 }
292
293 pub fn handle_event(&mut self, term: &mut Term, event: Event) -> Res<()> {
294 match event {
295 Event::Resize(w, h) => {
296 for screen in self.state.screens.iter_mut() {
297 screen.resize(w, h)?;
298 }
299
300 self.stage_redraw();
301 Ok(())
302 }
303 Event::Mouse(ref mouse) => {
304 if self.state.config.general.mouse_support
305 && self.handle_mouse_input(term, mouse)?
306 {
307 self.stage_redraw();
308 }
309 Ok(())
310 }
311 Event::Key(mut key) => {
312 if self.state.pending_cmd.is_none() {
313 self.state.current_cmd_log.clear();
314 }
315
316 if self.state.pending_cmd.is_none() {
317 self.state.current_cmd_log.clear();
318 }
319
320 if matches!(key.code, KeyCode::Char(_) | KeyCode::BackTab) {
325 key.modifiers = key.modifiers.difference(KeyModifiers::SHIFT);
326 }
327
328 if self.state.picker.is_some() {
329 self.handle_picker_input(key);
330 } else if self.state.prompt.state.focused {
331 self.state.prompt.handle_key(key);
332 } else {
333 self.handle_key_input(term, key)?;
334 }
335
336 self.stage_redraw();
337 Ok(())
338 }
339 _ => Ok(()),
340 }
341 }
342
343 pub fn redraw_now(&mut self, term: &mut Term) -> Res<()> {
344 if self.state.screens.last_mut().is_some() {
345 ui::ui(term, &mut self.state)?;
346 self.state.needs_redraw = false;
347 };
348
349 Ok(())
350 }
351
352 pub fn print_if_one_screen(&mut self, term: &mut Term) -> Res<bool> {
356 let screen = self.state.screens.last_mut().unwrap();
357 let (_, rows) = term.size().map_err(Error::Term)?;
358 let rows = rows.saturating_sub(1).into();
359 loop {
360 if !screen.fits_in(rows) {
361 return Ok(false);
362 }
363 if !screen.is_drawing() {
364 break;
365 }
366 if !screen.finish()? {
367 std::thread::sleep(Duration::from_millis(10));
368 }
369 }
370
371 screen::print_screen(term, screen)?;
372 Ok(true)
373 }
374
375 pub fn stage_redraw(&mut self) {
376 self.state.needs_redraw = true;
377 }
378
379 fn handle_key_input(&mut self, term: &mut Term, key: event::KeyEvent) -> Res<()> {
380 if key.kind != event::KeyEventKind::Press {
381 return Ok(());
382 }
383
384 let menu = match &self.state.pending_menu {
385 None => self.base_menu().unwrap_or(Menu::Root),
386 Some(menu) if menu.menu == Menu::Help => Menu::Root,
387 Some(menu) => menu.menu,
388 };
389
390 self.state.pending_keys.push((key.modifiers, key.code));
391
392 let matching_bindings = self
393 .state
394 .config
395 .bindings
396 .match_bindings(&menu, &self.state.pending_keys)
397 .collect::<Vec<_>>();
398
399 match matching_bindings[..] {
400 [binding] => {
401 if binding.keys == self.state.pending_keys {
402 let op = binding.op.clone();
403 self.state.pending_keys.clear();
404 self.handle_op(op, term)?;
405 }
406 }
407 [] => {
408 let feature = match self.state.pending_keys[..] {
409 [(KeyModifiers::NONE, KeyCode::Char(key))] if menu == Menu::Root => self
410 .state
411 .feature_keys
412 .iter()
413 .find(|(_, held)| **held == key)
414 .map(|(feature, _)| feature.clone()),
415 _ => None,
416 };
417 self.state.pending_keys.clear();
418 if let Some(feature) = feature {
419 crate::ops::editor::toggle_feature(self, &feature)?;
420 }
421 }
422 [_, ..] => (),
423 }
424
425 Ok(())
426 }
427
428 fn handle_mouse_input(&mut self, term: &mut Term, mouse: &MouseEvent) -> Res<bool> {
429 match mouse.kind {
430 MouseEventKind::Down(MouseButton::Left) => {
431 let click_y = mouse.row as usize;
432 if self.screen().is_valid_screen_line(click_y) {
433 let old_selected_item_id = self.screen().get_selected_item().id;
434 self.handle_op(Op::MoveToScreenLine(click_y), term)?;
435 let new_selected_item = self.screen().get_selected_item();
436
437 if old_selected_item_id == new_selected_item.id {
438 if new_selected_item.data.is_section() {
441 self.handle_op(Op::ToggleSection, term)?;
442 } else {
443 self.handle_op(Op::Show, term)?;
444 }
445 }
446 }
447 }
448 MouseEventKind::Down(MouseButton::Right) => {
449 let click_y = mouse.row as usize;
450 if self.screen().is_valid_screen_line(click_y) {
451 self.handle_op(Op::MoveToScreenLine(click_y), term)?;
452 self.handle_op(Op::Show, term)?;
453 }
454 }
455 MouseEventKind::ScrollUp => {
456 let scroll_lines = self.state.config.general.mouse_scroll_lines;
457 if scroll_lines > 0 {
458 self.screen_mut().scroll_view_up(scroll_lines);
459 }
460 }
461 MouseEventKind::ScrollDown => {
462 let scroll_lines = self.state.config.general.mouse_scroll_lines;
463 if scroll_lines > 0 {
464 self.screen_mut().scroll_view_down(scroll_lines);
465 }
466 }
467 _ => return Ok(false),
468 }
469
470 Ok(true)
471 }
472
473 pub(crate) fn handle_op(&mut self, op: Op, term: &mut Term) -> Res<()> {
474 let item_data = self.screen().selected_target();
475 let implementation = op.clone().implementation();
476
477 if let Some(mut action) = implementation.get_action(&item_data) {
478 let result = Rc::get_mut(&mut action).unwrap()(self, term);
479 self.handle_result(result)?;
480 if !self.state.inhibit_close_menu {
481 self.close_menu();
482 }
483 self.state.inhibit_close_menu = false;
484 }
485
486 Ok(())
487 }
488
489 fn handle_result<T>(&mut self, result: Res<T>) -> Res<()> {
490 match result {
491 Ok(_) => Ok(()),
492 Err(Error::NoMoreEvents) => Err(Error::NoMoreEvents),
493 Err(Error::PromptAborted) => Ok(()),
494 Err(error) => {
495 self.state
496 .current_cmd_log
497 .push(CmdLogEntry::Error(error.to_string()));
498
499 Ok(())
500 }
501 }
502 }
503
504 pub(crate) fn render_params(&self, size: (u16, u16)) -> RenderParams {
507 RenderParams {
508 size,
509 features: Rc::clone(&self.state.features),
510 context: self.state.context.clone(),
511 streaming: self.state.enable_async_cmds,
512 }
513 }
514
515 pub fn close_menu(&mut self) {
516 let hide = self.screen().menu.is_some() && !self.screen().show_menu;
517 self.state.pending_menu = self.base_menu().map(|menu| PendingMenu {
518 is_hidden: hide,
519 ..PendingMenu::init(menu)
520 });
521 }
522
523 pub(crate) fn base_menu(&self) -> Option<Menu> {
526 self.screen().menu.or_else(|| root_menu(&self.state.config))
527 }
528
529 pub fn inhibit_close_menu(&mut self) {
530 self.state.inhibit_close_menu = true;
531 }
532
533 pub(crate) fn screen_mut(&mut self) -> &mut Screen {
534 self.state.screens.last_mut().expect("No screen")
535 }
536
537 pub(crate) fn screen(&self) -> &Screen {
538 self.state.screens.last().expect("No screen")
539 }
540
541 pub fn display_info<S: Into<Cow<'static, str>>>(&mut self, message: S) {
543 self.state
544 .current_cmd_log
545 .push(CmdLogEntry::Info(message.into().into_owned()));
546 }
547
548 pub fn display_error<S: Into<Cow<'static, str>>>(&mut self, message: S) {
550 self.state
551 .current_cmd_log
552 .push(CmdLogEntry::Error(message.into().into_owned()));
553 }
554
555 pub fn run_cmd(&mut self, term: &mut Term, input: &[u8], cmd: Command) -> Res<()> {
558 self.run_cmd_async(term, input, cmd)?;
559 self.await_pending_cmd()?;
560 self.handle_pending_cmd()?;
561 Ok(())
562 }
563
564 pub fn run_cmd_async(&mut self, term: &mut Term, input: &[u8], mut cmd: Command) -> Res<()> {
567 cmd.env("CLICOLOR_FORCE", "1"); if self.state.pending_cmd.is_some() {
570 return Err(Error::CmdAlreadyRunning);
571 }
572
573 cmd.current_dir(self.state.repo.workdir().expect("No workdir"));
574
575 cmd.stdin(Stdio::piped());
576 cmd.stdout(Stdio::piped());
577 cmd.stderr(Stdio::piped());
578
579 let log_entry = self.state.current_cmd_log.push_cmd(&cmd);
580
581 ui::ui(term, &mut self.state)?;
582
583 let mut child = cmd.spawn().map_err(Error::SpawnCmd)?;
584
585 use std::io::Write;
586 child
587 .stdin
588 .take()
589 .unwrap()
590 .write_all(input)
591 .map_err(Error::Term)?;
592
593 self.state.pending_cmd = Some((child, log_entry));
594
595 if !self.state.enable_async_cmds {
596 self.await_pending_cmd()?;
597 }
598
599 Ok(())
600 }
601
602 fn await_pending_cmd(&mut self) -> Res<()> {
603 if let Some((child, _)) = &mut self.state.pending_cmd {
604 child.wait().map_err(Error::CouldntAwaitCmd)?;
605 }
606 Ok(())
607 }
608
609 fn handle_pending_cmd(&mut self) -> Res<()> {
611 let Some((ref mut child, ref mut log_rwlock)) = self.state.pending_cmd else {
612 return Ok(());
613 };
614
615 let Some(status) = child.try_wait().map_err(Error::CouldntAwaitCmd)? else {
616 return Ok(());
617 };
618
619 log::debug!("pending cmd finished with {status:?}");
620
621 let result = write_child_output_to_log(log_rwlock, child, status);
622 self.state.pending_cmd = None;
623 self.update_screens()?;
624 result?;
625
626 Ok(())
627 }
628
629 pub fn run_cmd_interactive(&mut self, term: &mut Term, mut cmd: Command) -> Res<()> {
630 cmd.env("CLICOLOR_FORCE", "1"); if self.state.pending_cmd.is_some() {
633 return Err(Error::CmdAlreadyRunning);
634 }
635
636 cmd.current_dir(self.state.repo.workdir().ok_or(Error::NoRepoWorkdir)?);
637
638 self.state
639 .current_cmd_log
640 .push_cmd_with_output(&cmd, "\n".into());
641 self.redraw_now(term)?;
642
643 eprint!("\r");
644
645 cmd.stderr(Stdio::piped());
647
648 term.reset_term_stay_on_alt_screeen(&self.state.config)
649 .map_err(Error::Term)?;
650
651 let mut child = cmd.spawn().map_err(Error::SpawnCmd)?;
652
653 drop(child.stdin.take());
655
656 let mut stderr = Vec::new();
657
658 tee(
659 child.stderr.as_mut(),
660 &mut [&mut std::io::stderr(), &mut stderr],
661 )
662 .map_err(Error::Term)?;
663
664 let status = child.wait().map_err(Error::CouldntAwaitCmd)?;
665
666 term.setup_term(&self.state.config).map_err(Error::Term)?;
667
668 let out_utf8 = String::from_utf8(strip_ansi_escapes::strip(stderr.clone()))
669 .expect("Error turning command output to String")
670 .into();
671
672 self.state.current_cmd_log.clear();
673 self.state
674 .current_cmd_log
675 .push_cmd_with_output(&cmd, out_utf8);
676
677 term.clear().map_err(Error::Term)?;
678 self.update_screens()?;
679
680 if !status.success() {
681 return Err(Error::CmdBadExit(
682 format!(
683 "{} {}",
684 cmd.get_program().to_string_lossy(),
685 cmd.get_args()
686 .map(|arg| arg.to_string_lossy())
687 .collect::<String>()
688 ),
689 status.code(),
690 ));
691 }
692
693 Ok(())
694 }
695
696 pub fn hide_menu(&mut self) {
697 if let Some(ref mut menu) = self.state.pending_menu {
698 menu.is_hidden = true;
699 }
700 }
701
702 fn unhide_menu(&mut self) {
703 if let Some(ref mut menu) = self.state.pending_menu {
704 menu.is_hidden = false;
705 }
706 }
707
708 pub(crate) fn selected_rev(&self) -> Option<Rev> {
709 self.screen().get_selected_item().data.rev()
710 }
711
712 pub(crate) fn prompt(&mut self, term: &mut Term, params: &PromptParams) -> Res<String> {
713 let default = (params.create_default_value)(self);
714
715 let prompt_text = match (params.prompt, &default) {
720 ("", _) => Cow::Borrowed(""),
721 (prompt, Some(default)) if !params.prefill => {
722 format!("{prompt} (default {default}):").into()
723 }
724 (prompt, _) => format!("{prompt}:").into(),
725 };
726
727 if params.hide_menu {
728 self.hide_menu();
729 }
730
731 self.state.prompt.set(prompt::PromptData { prompt_text });
732
733 if params.prefill
734 && let Some(default) = default
735 {
736 self.state.prompt.state.value = default;
737 self.state.prompt.state.move_to(usize::MAX);
738 }
739 self.state.prompt.start_history(params.history)?;
740
741 let result = self.handle_prompt(term, params);
742
743 self.unhide_menu();
744 self.state.prompt.reset();
745
746 result
747 }
748
749 fn handle_prompt(&mut self, term: &mut Term, params: &PromptParams) -> Res<String> {
750 self.redraw_now(term)?;
751
752 loop {
753 let event = term.read_event()?;
754 self.handle_event(term, event)?;
755
756 if self.state.prompt.state.status == text_input::Status::Done {
757 let value = get_prompt_result(params, self)?;
758 if let Some(kind) = params.history {
759 self.state.prompt.remember(kind, &value)?;
760 }
761 return Ok(value);
762 } else if self.state.prompt.state.status == text_input::Status::Aborted {
763 return Err(Error::PromptAborted);
764 }
765
766 self.redraw_now(term)?;
767 }
768 }
769
770 pub fn confirm(&mut self, term: &mut Term, prompt: &'static str) -> Res<()> {
771 self.hide_menu();
772 self.state.prompt.set(prompt::PromptData {
773 prompt_text: prompt.into(),
774 });
775
776 let result = self.handle_confirm(term);
777
778 self.unhide_menu();
779 self.state.prompt.reset();
780
781 result
782 }
783
784 fn handle_confirm(&mut self, term: &mut Term) -> Res<()> {
785 self.redraw_now(term)?;
786 loop {
787 let event = term.read_event()?;
788 self.handle_event(term, event)?;
789
790 match self.state.prompt.state.value.as_str() {
791 "y" => {
792 return Ok(());
793 }
794 "" => (),
795 _ => {
796 return Err(Error::PromptAborted);
797 }
798 }
799
800 self.redraw_now(term)?;
801 }
802 }
803
804 pub fn pick(&mut self, term: &mut Term, picker_state: PickerState) -> Res<Option<PickerData>> {
834 Ok(self
835 .pick_or_set_key(term, picker_state)?
836 .map(|picked| match picked {
837 Picked::Item(data) => data,
838 Picked::KeySet(..) => unreachable!("only a picker assigning keys sets them"),
839 }))
840 }
841
842 pub(crate) fn pick_or_set_key(
844 &mut self,
845 term: &mut Term,
846 picker_state: PickerState,
847 ) -> Res<Option<Picked>> {
848 self.state.picker = Some(picker_state);
849 let result = self.handle_picker(term);
850
851 self.state.picker = None;
852
853 result
854 }
855
856 pub fn pick_commit(&mut self, term: &mut Term) -> Res<Option<String>> {
860 let params = self.render_params(self.screen().size);
861 let selected = match &self.screen().get_selected_item().data {
862 crate::item_data::ItemData::Commit { oid, .. } => Some(oid.clone()),
863 _ => None,
864 };
865 let params = RenderParams {
868 streaming: params.streaming && selected.is_none(),
869 ..params
870 };
871 self.state.screens.push(screen::pager::log(
872 Arc::clone(&self.state.config),
873 Rc::clone(&self.state.repo),
874 params,
875 COMMIT_PICK_LIMIT,
876 None,
877 None,
878 )?);
879 self.screen_mut().menu = Some(Menu::CommitPicker);
880 self.close_menu();
881
882 if let Some(selected) = selected {
884 self.screen_mut().select_matching(|data| {
885 matches!(data, crate::item_data::ItemData::Commit { oid, .. } if *oid == selected)
886 });
887 }
888
889 let depth = self.state.screens.len();
890 self.redraw_now(term)?;
891
892 loop {
893 let event = term.read_event()?;
894 self.handle_event(term, event)?;
895
896 if let Some(oid) = self.state.picked_commit.take() {
897 self.state.screens.truncate(depth - 1);
898 self.close_menu();
899 return Ok(Some(oid));
900 }
901 if self.state.screens.len() < depth {
902 return Ok(None); }
904
905 self.redraw_now(term)?;
906 }
907 }
908
909 fn handle_picker(&mut self, term: &mut Term) -> Res<Option<Picked>> {
910 self.redraw_now(term)?;
911
912 loop {
913 let event = term.read_event()?;
914 self.handle_event(term, event)?;
915
916 if let Some(ref picker) = self.state.picker {
917 let selected = || picker.selected().map(|item| item.data.clone());
918 match *picker.status() {
919 PickerStatus::Done => return Ok(selected().map(Picked::Item)),
920 PickerStatus::KeySet(key) => {
921 return Ok(selected().map(|data| Picked::KeySet(key, data)));
922 }
923 PickerStatus::Cancelled => return Ok(None),
925 PickerStatus::Active => (),
926 }
927 }
928
929 self.redraw_now(term)?;
930 }
931 }
932
933 fn handle_picker_input(&mut self, key: event::KeyEvent) {
934 if key.kind != event::KeyEventKind::Press {
935 return;
936 }
937
938 if let Some(ref mut picker) = self.state.picker {
939 let bindings = &self.state.config.picker_bindings;
940 let key_combo = vec![(key.modifiers, key.code)];
941 let typed = match (key.code, key.modifiers) {
942 (
943 event::KeyCode::Char(c),
944 event::KeyModifiers::NONE | event::KeyModifiers::SHIFT,
945 ) => Some(c),
946 _ => None,
947 };
948
949 if bindings.next.iter().any(|b| b == &key_combo) {
950 picker.next();
951 } else if bindings.previous.iter().any(|b| b == &key_combo) {
952 picker.previous();
953 } else if bindings.done.iter().any(|b| b == &key_combo) {
954 picker.done();
955 } else if bindings.cancel.iter().any(|b| b == &key_combo) {
956 picker.cancel();
957 } else if picker.assigns_keys() {
958 if let Some(c) = typed {
959 picker.set_key(c);
960 }
961 } else {
962 picker.input_state.handle_key_event(key);
964 picker.update_filter();
965 }
966 }
967 }
968}
969
970fn get_prompt_result(params: &PromptParams, app: &mut App) -> Res<String> {
971 let input = app.state.prompt.state.value.as_str();
972 if params.prefill {
973 return Ok(input.to_owned());
974 }
975
976 let default_value = (params.create_default_value)(app);
977
978 let value = match (input, &default_value) {
979 ("", None) => "",
980 ("", Some(selected)) => selected,
981 (value, _) => value,
982 };
983
984 Ok(value.to_string())
985}
986
987fn tee(maybe_input: Option<&mut impl Read>, outputs: &mut [&mut dyn Write]) -> std::io::Result<()> {
988 let Some(input) = maybe_input else {
989 return Ok(());
990 };
991
992 let mut buf = [0u8; 1024];
993
994 loop {
995 let num_read = input.read(&mut buf)?;
996 if num_read == 0 {
997 break;
998 }
999
1000 let buf = &buf[..num_read];
1001 for output in &mut *outputs {
1002 output.write_all(buf)?;
1003 }
1004 }
1005
1006 Ok(())
1007}
1008
1009fn rebase_todo_screen(
1010 config: &Arc<Config>,
1011 repo: &Rc<Repository>,
1012 params: RenderParams,
1013 todo: RebaseTodo,
1014) -> Res<Screen> {
1015 screen::rebase_todo::create(
1016 Arc::clone(config),
1017 Rc::clone(repo),
1018 params,
1019 Rc::new(RefCell::new(todo)),
1020 )
1021}
1022
1023const COMMIT_PICK_LIMIT: usize = 256;
1025
1026pub(crate) fn root_menu(config: &Config) -> Option<Menu> {
1027 if config.general.always_show_help.enabled {
1028 Some(Menu::Help)
1029 } else {
1030 None
1031 }
1032}
1033
1034fn write_child_output_to_log(
1035 log_rwlock: &mut Arc<RwLock<CmdLogEntry>>,
1036 child: &mut Child,
1037 status: std::process::ExitStatus,
1038) -> Res<()> {
1039 let mut log = log_rwlock.write().unwrap();
1040
1041 let CmdLogEntry::Cmd { args, out: out_log } = log.deref_mut() else {
1042 unreachable!("pending_cmd is always CmdLogEntry::Cmd variant");
1043 };
1044
1045 drop(child.stdin.take());
1046
1047 let mut out_bytes = vec![];
1048 log::debug!("Reading stderr");
1049
1050 child
1051 .stderr
1052 .take()
1053 .unwrap()
1054 .read_to_end(&mut out_bytes)
1055 .map_err(Error::CouldntReadCmdOutput)?;
1056
1057 child
1058 .stdout
1059 .take()
1060 .unwrap()
1061 .read_to_end(&mut out_bytes)
1062 .map_err(Error::CouldntReadCmdOutput)?;
1063
1064 let out_string = String::from_utf8_lossy(&out_bytes).to_string();
1065 *out_log = Some(out_string.into());
1066
1067 if !status.success() {
1068 return Err(Error::CmdBadExit(args.to_string(), status.code()));
1069 }
1070
1071 Ok(())
1072}
1073
1074type DefaultFn = Box<dyn Fn(&App) -> Option<String>>;
1075
1076pub(crate) struct PromptParams {
1077 pub prompt: &'static str,
1078 pub create_default_value: DefaultFn,
1079 pub hide_menu: bool,
1080 pub prefill: bool,
1083 pub history: Option<prompt::HistoryKind>,
1084}
1085
1086impl Default for PromptParams {
1087 fn default() -> Self {
1088 Self {
1089 prompt: "",
1090 create_default_value: Box::new(|_| None),
1091 hide_menu: true,
1092 prefill: false,
1093 history: None,
1094 }
1095 }
1096}