Skip to main content

gitu/
app.rs

1use std::borrow::Cow;
2use std::cell::RefCell;
3use std::ffi::OsStr;
4use std::io::Read;
5use std::io::Write;
6use std::ops::DerefMut;
7use std::process::Child;
8use std::process::Command;
9use std::process::Stdio;
10use std::rc::Rc;
11use std::sync::Arc;
12use std::sync::RwLock;
13use std::time::Duration;
14
15use arboard::Clipboard;
16use crossterm::event;
17use crossterm::event::Event;
18use crossterm::event::KeyCode;
19use crossterm::event::KeyModifiers;
20use crossterm::event::MouseButton;
21use crossterm::event::MouseEvent;
22use crossterm::event::MouseEventKind;
23use git2::Repository;
24
25use crate::cli;
26use crate::cmd_log::CmdLog;
27use crate::cmd_log::CmdLogEntry;
28use crate::config::Config;
29use crate::error::Error;
30use crate::file_watcher::FileWatcher;
31use crate::item_data::Rev;
32use crate::items::RenderParams;
33use crate::menu::Menu;
34use crate::menu::PendingMenu;
35use crate::ops::Op;
36use crate::picker::PickerData;
37use crate::picker::PickerState;
38use crate::prompt;
39use crate::rebase_todo::RebaseTodo;
40use crate::screen;
41use crate::screen::Screen;
42use crate::screen::pager::Paged;
43use crate::term::Term;
44use crate::text_input;
45use crate::ui;
46
47use super::Res;
48
49pub(crate) struct State {
50    pub repo: Rc<Repository>,
51    pub config: Arc<Config>,
52    pub pending_keys: Vec<(KeyModifiers, KeyCode)>,
53    pub quit: bool,
54    /// What gitu exits with. Non-zero only to tell git, which is waiting on
55    /// gitu as its sequence editor, to call the rebase off.
56    pub exit_code: i32,
57    pub screens: Vec<Screen>,
58    pub pending_menu: Option<PendingMenu>,
59    pending_cmd: Option<(Child, Arc<RwLock<CmdLogEntry>>)>,
60    enable_async_cmds: bool,
61    pub current_cmd_log: CmdLog,
62    pub prompt: prompt::Prompt,
63    pub picker: Option<PickerState>,
64    /// The commit `select_commit` named, read by [`App::pick_commit`].
65    pub picked_commit: Option<String>,
66    /// Renderer features chosen in-session, overlaying the user's own
67    /// configuration. Never written to disk: quitting returns them to it.
68    pub features: Rc<[String]>,
69    /// How much of a file to ask git for around each change (`-U8`, `-W`),
70    /// chosen in-session. `None` is git's own default.
71    pub context: Option<Rc<str>>,
72    pub clipboard: Option<Clipboard>,
73    needs_redraw: bool,
74    file_watcher: Option<FileWatcher>,
75    inhibit_close_menu: bool,
76}
77
78pub struct App {
79    pub(crate) state: State,
80}
81
82impl App {
83    pub fn create(
84        repo: Rc<Repository>,
85        size: (u16, u16),
86        args: &cli::Args,
87        config: Arc<Config>,
88        enable_async_cmds: bool,
89        piped: Option<Paged>,
90    ) -> Res<Self> {
91        let params = RenderParams {
92            size,
93            features: Rc::from([]),
94            context: None,
95        };
96        let screens = match args.command {
97            // `--pager`: git's output is already in hand, with the command it ran.
98            _ if let Some(paged) = piped => {
99                vec![screen::pager::create(
100                    Arc::clone(&config),
101                    Rc::clone(&repo),
102                    params,
103                    paged,
104                )?]
105            }
106            Some(cli::Commands::Show { ref reference }) => {
107                vec![screen::show::create(
108                    Arc::clone(&config),
109                    Rc::clone(&repo),
110                    params.clone(),
111                    reference.clone(),
112                    None,
113                )?]
114            }
115            Some(cli::Commands::Blame { ref rev, ref file }) => {
116                vec![screen::blame::create(
117                    Arc::clone(&config),
118                    Rc::clone(&repo),
119                    params.clone(),
120                    file.clone(),
121                    rev.clone(),
122                    None,
123                )?]
124            }
125            Some(cli::Commands::Rebase { ref upstream }) => {
126                let todo = RebaseTodo::capture(&repo, OsStr::new(upstream), &[])?;
127                vec![rebase_todo_screen(&config, &repo, params.clone(), todo)?]
128            }
129            Some(cli::Commands::SequenceEditor { ref file }) => {
130                let todo = RebaseTodo::read(&repo, file)?;
131                vec![rebase_todo_screen(&config, &repo, params.clone(), todo)?]
132            }
133            // `completion` prints its script and exits in `main`, before the app is ever built.
134            Some(cli::Commands::Completion { .. }) => {
135                unreachable!("completion is handled before the app starts")
136            }
137            None => vec![screen::status::create(
138                Arc::clone(&config),
139                Rc::clone(&repo),
140                params,
141            )?],
142        };
143
144        let clipboard = Clipboard::new()
145            .inspect_err(|e| log::warn!("Couldn't initialize clipboard: {e}"))
146            .ok();
147        let prompt = prompt::Prompt::with_history(repo.path().join("gitu"));
148
149        let mut app = Self {
150            state: State {
151                repo,
152                config,
153                pending_keys: vec![],
154                enable_async_cmds,
155                quit: false,
156                exit_code: 0,
157                screens,
158                pending_cmd: None,
159                pending_menu: None,
160                current_cmd_log: CmdLog::new(),
161                prompt,
162                picker: None,
163                picked_commit: None,
164                features: Rc::from([]),
165                context: None,
166                clipboard,
167                file_watcher: None,
168                needs_redraw: true,
169                inhibit_close_menu: false,
170            },
171        };
172
173        // The menu a screen imposes is in force from the first key on, not from
174        // the first key that closes a menu.
175        app.close_menu();
176        app.state.file_watcher = app.init_file_watcher()?;
177        Ok(app)
178    }
179
180    fn init_file_watcher(&mut self) -> Res<Option<FileWatcher>> {
181        if !self.state.config.general.refresh_on_file_change.enabled {
182            return Ok(None);
183        }
184
185        let hiding_untracked_files = !self
186            .state
187            .repo
188            .config()
189            .map_err(Error::ReadGitConfig)?
190            .get_bool("status.showUntrackedFiles")
191            .unwrap_or(true);
192
193        if hiding_untracked_files {
194            self.display_info("File watcher disabled (status.showUntrackedFiles is off)");
195
196            return Ok(None);
197        }
198
199        Ok(
200            FileWatcher::new(self.state.repo.workdir().expect("Bare repos unhandled"))
201                .inspect_err(|err| {
202                    self.display_error(err.to_string());
203                    self.display_info("File watcher disabled");
204                })
205                .ok(),
206        )
207    }
208
209    pub fn run(&mut self, term: &mut Term, max_tick_delay: Duration) -> Res<()> {
210        while !self.state.quit {
211            let event = if term.poll_event(max_tick_delay)? {
212                Some(term.read_event()?)
213            } else {
214                None
215            };
216
217            self.update(term, event)?;
218        }
219
220        Ok(())
221    }
222
223    pub fn update(&mut self, term: &mut Term, event: Option<Event>) -> Res<()> {
224        if let Some(e) = event {
225            self.handle_event(term, e)?;
226        }
227
228        if let Some(file_watcher) = &mut self.state.file_watcher
229            && file_watcher.pending_updates()
230        {
231            self.update_screens()?;
232        }
233
234        let handle_pending_cmd_result = self.handle_pending_cmd();
235        self.handle_result(handle_pending_cmd_result)?;
236
237        if self.state.needs_redraw {
238            self.redraw_now(term)?;
239        }
240
241        Ok(())
242    }
243
244    pub fn update_screens(&mut self) -> Res<()> {
245        self.rebuild_screens(Screen::refresh)
246    }
247
248    /// Rebuild every screen, leaving each cursor where it was in the diff rather
249    /// than on the line number it happened to occupy. For a re-render of the
250    /// same content (a renderer feature turned on or off), the rows move but the
251    /// place in the patch does not.
252    pub fn rerender_screens(&mut self) -> Res<()> {
253        self.rebuild_screens(Screen::refresh_keeping_position)
254    }
255
256    fn rebuild_screens(&mut self, rebuild: impl Fn(&mut Screen) -> Res<()>) -> Res<()> {
257        let features = Rc::clone(&self.state.features);
258        for screen in &mut self.state.screens {
259            screen.features = Rc::clone(&features);
260            screen.context = self.state.context.clone();
261            rebuild(screen)?;
262        }
263
264        self.stage_redraw();
265        Ok(())
266    }
267
268    pub fn handle_event(&mut self, term: &mut Term, event: Event) -> Res<()> {
269        match event {
270            Event::Resize(w, h) => {
271                for screen in self.state.screens.iter_mut() {
272                    screen.resize(w, h)?;
273                }
274
275                self.stage_redraw();
276                Ok(())
277            }
278            Event::Mouse(ref mouse) => {
279                if self.state.config.general.mouse_support
280                    && self.handle_mouse_input(term, mouse)?
281                {
282                    self.stage_redraw();
283                }
284                Ok(())
285            }
286            Event::Key(mut key) => {
287                if self.state.pending_cmd.is_none() {
288                    self.state.current_cmd_log.clear();
289                }
290
291                if self.state.pending_cmd.is_none() {
292                    self.state.current_cmd_log.clear();
293                }
294
295                // The key received changes as shift is pressed: '/' becomes '?'
296                // on a US keyboard, and tab becomes backtab. The modifier is
297                // redundant there, and on the other keys it is the only sign
298                // shift was held.
299                if matches!(key.code, KeyCode::Char(_) | KeyCode::BackTab) {
300                    key.modifiers = key.modifiers.difference(KeyModifiers::SHIFT);
301                }
302
303                if self.state.picker.is_some() {
304                    self.handle_picker_input(key);
305                } else if self.state.prompt.state.focused {
306                    self.state.prompt.handle_key(key);
307                } else {
308                    self.handle_key_input(term, key)?;
309                }
310
311                self.stage_redraw();
312                Ok(())
313            }
314            _ => Ok(()),
315        }
316    }
317
318    pub fn redraw_now(&mut self, term: &mut Term) -> Res<()> {
319        if self.state.screens.last_mut().is_some() {
320            ui::ui(term, &mut self.state)?;
321            self.state.needs_redraw = false;
322        };
323
324        Ok(())
325    }
326
327    pub fn stage_redraw(&mut self) {
328        self.state.needs_redraw = true;
329    }
330
331    fn handle_key_input(&mut self, term: &mut Term, key: event::KeyEvent) -> Res<()> {
332        if key.kind != event::KeyEventKind::Press {
333            return Ok(());
334        }
335
336        let menu = match &self.state.pending_menu {
337            None => self.base_menu().unwrap_or(Menu::Root),
338            Some(menu) if menu.menu == Menu::Help => Menu::Root,
339            Some(menu) => menu.menu,
340        };
341
342        self.state.pending_keys.push((key.modifiers, key.code));
343
344        let matching_bindings = self
345            .state
346            .config
347            .bindings
348            .match_bindings(&menu, &self.state.pending_keys)
349            .collect::<Vec<_>>();
350
351        match matching_bindings[..] {
352            [binding] => {
353                if binding.keys == self.state.pending_keys {
354                    let op = binding.op.clone();
355                    self.state.pending_keys.clear();
356                    self.handle_op(op, term)?;
357                }
358            }
359            [] => self.state.pending_keys.clear(),
360            [_, ..] => (),
361        }
362
363        Ok(())
364    }
365
366    fn handle_mouse_input(&mut self, term: &mut Term, mouse: &MouseEvent) -> Res<bool> {
367        match mouse.kind {
368            MouseEventKind::Down(MouseButton::Left) => {
369                let click_y = mouse.row as usize;
370                if self.screen().is_valid_screen_line(click_y) {
371                    let old_selected_item_id = self.screen().get_selected_item().id;
372                    self.handle_op(Op::MoveToScreenLine(click_y), term)?;
373                    let new_selected_item = self.screen().get_selected_item();
374
375                    if old_selected_item_id == new_selected_item.id {
376                        // If the item clicked was already the current item, then try to
377                        // toggle it if it's a section or show it.
378                        if new_selected_item.data.is_section() {
379                            self.handle_op(Op::ToggleSection, term)?;
380                        } else {
381                            self.handle_op(Op::Show, term)?;
382                        }
383                    }
384                }
385            }
386            MouseEventKind::Down(MouseButton::Right) => {
387                let click_y = mouse.row as usize;
388                if self.screen().is_valid_screen_line(click_y) {
389                    self.handle_op(Op::MoveToScreenLine(click_y), term)?;
390                    self.handle_op(Op::Show, term)?;
391                }
392            }
393            MouseEventKind::ScrollUp => {
394                let scroll_lines = self.state.config.general.mouse_scroll_lines;
395                if scroll_lines > 0 {
396                    self.screen_mut().scroll_view_up(scroll_lines);
397                }
398            }
399            MouseEventKind::ScrollDown => {
400                let scroll_lines = self.state.config.general.mouse_scroll_lines;
401                if scroll_lines > 0 {
402                    self.screen_mut().scroll_view_down(scroll_lines);
403                }
404            }
405            _ => return Ok(false),
406        }
407
408        Ok(true)
409    }
410
411    pub(crate) fn handle_op(&mut self, op: Op, term: &mut Term) -> Res<()> {
412        let item_data = self.screen().selected_target();
413        let implementation = op.clone().implementation();
414
415        if let Some(mut action) = implementation.get_action(&item_data) {
416            let result = Rc::get_mut(&mut action).unwrap()(self, term);
417            self.handle_result(result)?;
418            if !self.state.inhibit_close_menu {
419                self.close_menu();
420            }
421            self.state.inhibit_close_menu = false;
422        }
423
424        Ok(())
425    }
426
427    fn handle_result<T>(&mut self, result: Res<T>) -> Res<()> {
428        match result {
429            Ok(_) => Ok(()),
430            Err(Error::NoMoreEvents) => Err(Error::NoMoreEvents),
431            Err(Error::PromptAborted) => Ok(()),
432            Err(error) => {
433                self.state
434                    .current_cmd_log
435                    .push(CmdLogEntry::Error(error.to_string()));
436
437                Ok(())
438            }
439        }
440    }
441
442    /// What a screen opened now should render with: the viewport it will get,
443    /// and the features currently selected.
444    pub(crate) fn render_params(&self, size: (u16, u16)) -> RenderParams {
445        RenderParams {
446            size,
447            features: Rc::clone(&self.state.features),
448            context: self.state.context.clone(),
449        }
450    }
451
452    pub fn close_menu(&mut self) {
453        let hide = self.screen().menu.is_some() && !self.screen().show_menu;
454        self.state.pending_menu = self.base_menu().map(|menu| PendingMenu {
455            is_hidden: hide,
456            ..PendingMenu::init(menu)
457        });
458    }
459
460    /// The menu to fall back to once a pending one closes: a screen with its own
461    /// keymap (the interactive rebase todo) imposes it for as long as it is up.
462    pub(crate) fn base_menu(&self) -> Option<Menu> {
463        self.screen().menu.or_else(|| root_menu(&self.state.config))
464    }
465
466    pub fn inhibit_close_menu(&mut self) {
467        self.state.inhibit_close_menu = true;
468    }
469
470    pub(crate) fn screen_mut(&mut self) -> &mut Screen {
471        self.state.screens.last_mut().expect("No screen")
472    }
473
474    pub(crate) fn screen(&self) -> &Screen {
475        self.state.screens.last().expect("No screen")
476    }
477
478    /// Displays an `Info` message to the CmdLog.
479    pub fn display_info<S: Into<Cow<'static, str>>>(&mut self, message: S) {
480        self.state
481            .current_cmd_log
482            .push(CmdLogEntry::Info(message.into().into_owned()));
483    }
484
485    /// Displays an `Error` message to the CmdLog.
486    pub fn display_error<S: Into<Cow<'static, str>>>(&mut self, message: S) {
487        self.state
488            .current_cmd_log
489            .push(CmdLogEntry::Error(message.into().into_owned()));
490    }
491
492    /// Runs a `Command` and handles its output.
493    /// Will block awaiting its completion.
494    pub fn run_cmd(&mut self, term: &mut Term, input: &[u8], cmd: Command) -> Res<()> {
495        self.run_cmd_async(term, input, cmd)?;
496        self.await_pending_cmd()?;
497        self.handle_pending_cmd()?;
498        Ok(())
499    }
500
501    /// Runs a `Command` and handles its output asynchronously (if async commands are enabled).
502    /// Will return `Ok(())` if one is already running.
503    pub fn run_cmd_async(&mut self, term: &mut Term, input: &[u8], mut cmd: Command) -> Res<()> {
504        cmd.env("CLICOLOR_FORCE", "1"); // No guarantee, but modern tools seem to implement this
505
506        if self.state.pending_cmd.is_some() {
507            return Err(Error::CmdAlreadyRunning);
508        }
509
510        cmd.current_dir(self.state.repo.workdir().expect("No workdir"));
511
512        cmd.stdin(Stdio::piped());
513        cmd.stdout(Stdio::piped());
514        cmd.stderr(Stdio::piped());
515
516        let log_entry = self.state.current_cmd_log.push_cmd(&cmd);
517
518        ui::ui(term, &mut self.state)?;
519
520        let mut child = cmd.spawn().map_err(Error::SpawnCmd)?;
521
522        use std::io::Write;
523        child
524            .stdin
525            .take()
526            .unwrap()
527            .write_all(input)
528            .map_err(Error::Term)?;
529
530        self.state.pending_cmd = Some((child, log_entry));
531
532        if !self.state.enable_async_cmds {
533            self.await_pending_cmd()?;
534        }
535
536        Ok(())
537    }
538
539    fn await_pending_cmd(&mut self) -> Res<()> {
540        if let Some((child, _)) = &mut self.state.pending_cmd {
541            child.wait().map_err(Error::CouldntAwaitCmd)?;
542        }
543        Ok(())
544    }
545
546    /// Handles any pending_cmd in State without blocking. Returns `true` if a cmd was handled.
547    fn handle_pending_cmd(&mut self) -> Res<()> {
548        let Some((ref mut child, ref mut log_rwlock)) = self.state.pending_cmd else {
549            return Ok(());
550        };
551
552        let Some(status) = child.try_wait().map_err(Error::CouldntAwaitCmd)? else {
553            return Ok(());
554        };
555
556        log::debug!("pending cmd finished with {status:?}");
557
558        let result = write_child_output_to_log(log_rwlock, child, status);
559        self.state.pending_cmd = None;
560        self.update_screens()?;
561        result?;
562
563        Ok(())
564    }
565
566    pub fn run_cmd_interactive(&mut self, term: &mut Term, mut cmd: Command) -> Res<()> {
567        cmd.env("CLICOLOR_FORCE", "1"); // No guarantee, but modern tools seem to implement this
568
569        if self.state.pending_cmd.is_some() {
570            return Err(Error::CmdAlreadyRunning);
571        }
572
573        cmd.current_dir(self.state.repo.workdir().ok_or(Error::NoRepoWorkdir)?);
574
575        self.state
576            .current_cmd_log
577            .push_cmd_with_output(&cmd, "\n".into());
578        self.redraw_now(term)?;
579
580        eprint!("\r");
581
582        // Redirect stderr so we can capture it via `Child::wait_with_output()`
583        cmd.stderr(Stdio::piped());
584
585        term.reset_term_stay_on_alt_screeen(&self.state.config)
586            .map_err(Error::Term)?;
587
588        let mut child = cmd.spawn().map_err(Error::SpawnCmd)?;
589
590        // Drop stdin as `Child::wait_with_output` would
591        drop(child.stdin.take());
592
593        let mut stderr = Vec::new();
594
595        tee(
596            child.stderr.as_mut(),
597            &mut [&mut std::io::stderr(), &mut stderr],
598        )
599        .map_err(Error::Term)?;
600
601        let status = child.wait().map_err(Error::CouldntAwaitCmd)?;
602
603        term.setup_term(&self.state.config).map_err(Error::Term)?;
604
605        let out_utf8 = String::from_utf8(strip_ansi_escapes::strip(stderr.clone()))
606            .expect("Error turning command output to String")
607            .into();
608
609        self.state.current_cmd_log.clear();
610        self.state
611            .current_cmd_log
612            .push_cmd_with_output(&cmd, out_utf8);
613
614        term.clear().map_err(Error::Term)?;
615        self.update_screens()?;
616
617        if !status.success() {
618            return Err(Error::CmdBadExit(
619                format!(
620                    "{} {}",
621                    cmd.get_program().to_string_lossy(),
622                    cmd.get_args()
623                        .map(|arg| arg.to_string_lossy())
624                        .collect::<String>()
625                ),
626                status.code(),
627            ));
628        }
629
630        Ok(())
631    }
632
633    pub fn hide_menu(&mut self) {
634        if let Some(ref mut menu) = self.state.pending_menu {
635            menu.is_hidden = true;
636        }
637    }
638
639    fn unhide_menu(&mut self) {
640        if let Some(ref mut menu) = self.state.pending_menu {
641            menu.is_hidden = false;
642        }
643    }
644
645    pub(crate) fn selected_rev(&self) -> Option<Rev> {
646        self.screen().get_selected_item().data.rev()
647    }
648
649    pub(crate) fn prompt(&mut self, term: &mut Term, params: &PromptParams) -> Res<String> {
650        let default = (params.create_default_value)(self);
651
652        // A prompt with nothing to say says nothing: the key that opened it
653        // already said what is being answered. Prefilled, the default is the
654        // answer being edited rather than the one an empty answer falls back
655        // to — it is on the line, so naming it again says nothing either.
656        let prompt_text = match (params.prompt, &default) {
657            ("", _) => Cow::Borrowed(""),
658            (prompt, Some(default)) if !params.prefill => {
659                format!("{prompt} (default {default}):").into()
660            }
661            (prompt, _) => format!("{prompt}:").into(),
662        };
663
664        if params.hide_menu {
665            self.hide_menu();
666        }
667
668        self.state.prompt.set(prompt::PromptData { prompt_text });
669
670        if params.prefill
671            && let Some(default) = default
672        {
673            self.state.prompt.state.value = default;
674            self.state.prompt.state.move_to(usize::MAX);
675        }
676        self.state.prompt.start_history(params.history)?;
677
678        let result = self.handle_prompt(term, params);
679
680        self.unhide_menu();
681        self.state.prompt.reset();
682
683        result
684    }
685
686    fn handle_prompt(&mut self, term: &mut Term, params: &PromptParams) -> Res<String> {
687        self.redraw_now(term)?;
688
689        loop {
690            let event = term.read_event()?;
691            self.handle_event(term, event)?;
692
693            if self.state.prompt.state.status == text_input::Status::Done {
694                let value = get_prompt_result(params, self)?;
695                self.state.prompt.remember(&value)?;
696                return Ok(value);
697            } else if self.state.prompt.state.status == text_input::Status::Aborted {
698                return Err(Error::PromptAborted);
699            }
700
701            self.redraw_now(term)?;
702        }
703    }
704
705    pub fn confirm(&mut self, term: &mut Term, prompt: &'static str) -> Res<()> {
706        self.hide_menu();
707        self.state.prompt.set(prompt::PromptData {
708            prompt_text: prompt.into(),
709        });
710
711        let result = self.handle_confirm(term);
712
713        self.unhide_menu();
714        self.state.prompt.reset();
715
716        result
717    }
718
719    fn handle_confirm(&mut self, term: &mut Term) -> Res<()> {
720        self.redraw_now(term)?;
721        loop {
722            let event = term.read_event()?;
723            self.handle_event(term, event)?;
724
725            match self.state.prompt.state.value.as_str() {
726                "y" => {
727                    return Ok(());
728                }
729                "" => (),
730                _ => {
731                    return Err(Error::PromptAborted);
732                }
733            }
734
735            self.redraw_now(term)?;
736        }
737    }
738
739    /// Show a picker and wait for user to select an item or cancel.
740    ///
741    /// Returns:
742    /// - `Ok(Some(data))` - User selected an item
743    /// - `Ok(None)` - User cancelled (Esc or Ctrl-C)
744    /// - `Err(e)` - An error occurred
745    ///
746    /// # Example
747    /// ```ignore
748    /// let items = vec![
749    ///     PickerItem::new("main", PickerData::Item("main".to_string())),
750    ///     PickerItem::new("develop", PickerData::Item("develop".to_string())),
751    /// ];
752    /// let picker = PickerState::new("Select branch", items, false);
753    ///
754    /// match app.pick(term, picker)? {
755    ///     Some(PickerData::Item(name)) => {
756    ///         // User selected a branch
757    ///         println!("Selected: {}", name);
758    ///     }
759    ///     Some(PickerData::CustomInput(_)) => {
760    ///         // Should not happen when allow_custom_input is false
761    ///     }
762    ///     None => {
763    ///         // User cancelled
764    ///         println!("Cancelled");
765    ///     }
766    /// }
767    /// ```
768    pub fn pick(&mut self, term: &mut Term, picker_state: PickerState) -> Res<Option<PickerData>> {
769        self.state.picker = Some(picker_state);
770        let result = self.handle_picker(term);
771
772        self.state.picker = None;
773
774        result
775    }
776
777    /// Choose a commit by moving around a log view: the screen is pushed with
778    /// its own keymap, and the loop runs until `select_commit` names one or the
779    /// screen is closed.
780    pub fn pick_commit(&mut self, term: &mut Term) -> Res<Option<String>> {
781        let params = self.render_params(self.screen().size);
782        let selected = match &self.screen().get_selected_item().data {
783            crate::item_data::ItemData::Commit { oid, .. } => Some(oid.clone()),
784            _ => None,
785        };
786        self.state.screens.push(screen::log::create(
787            Arc::clone(&self.state.config),
788            Rc::clone(&self.state.repo),
789            params,
790            COMMIT_PICK_LIMIT,
791            None,
792            None,
793        )?);
794        self.screen_mut().menu = Some(Menu::CommitPicker);
795        self.close_menu();
796
797        // Start on the commit that was already selected, if there was one.
798        if let Some(selected) = selected {
799            self.screen_mut().select_matching(|data| {
800                matches!(data, crate::item_data::ItemData::Commit { oid, .. } if *oid == selected)
801            });
802        }
803
804        let depth = self.state.screens.len();
805        self.redraw_now(term)?;
806
807        loop {
808            let event = term.read_event()?;
809            self.handle_event(term, event)?;
810
811            if let Some(oid) = self.state.picked_commit.take() {
812                self.state.screens.truncate(depth - 1);
813                self.close_menu();
814                return Ok(Some(oid));
815            }
816            if self.state.screens.len() < depth {
817                return Ok(None); // The screen was closed: nothing picked.
818            }
819
820            self.redraw_now(term)?;
821        }
822    }
823
824    fn handle_picker(&mut self, term: &mut Term) -> Res<Option<PickerData>> {
825        self.redraw_now(term)?;
826
827        loop {
828            let event = term.read_event()?;
829            self.handle_event(term, event)?;
830
831            if let Some(ref picker) = self.state.picker {
832                if picker.is_done() {
833                    // User selected an item
834                    return Ok(picker.selected().map(|item| item.data.clone()));
835                } else if picker.is_cancelled() {
836                    // User cancelled - this is not an error
837                    return Ok(None);
838                }
839            }
840
841            self.redraw_now(term)?;
842        }
843    }
844
845    fn handle_picker_input(&mut self, key: event::KeyEvent) {
846        if key.kind != event::KeyEventKind::Press {
847            return;
848        }
849
850        if let Some(ref mut picker) = self.state.picker {
851            let bindings = &self.state.config.picker_bindings;
852            let key_combo = vec![(key.modifiers, key.code)];
853
854            if bindings.next.iter().any(|b| b == &key_combo) {
855                picker.next();
856            } else if bindings.previous.iter().any(|b| b == &key_combo) {
857                picker.previous();
858            } else if bindings.done.iter().any(|b| b == &key_combo) {
859                picker.done();
860            } else if bindings.cancel.iter().any(|b| b == &key_combo) {
861                picker.cancel();
862            } else {
863                // Text input - delegate to text state
864                picker.input_state.handle_key_event(key);
865                picker.update_filter();
866            }
867        }
868    }
869}
870
871fn get_prompt_result(params: &PromptParams, app: &mut App) -> Res<String> {
872    let input = app.state.prompt.state.value.as_str();
873    if params.prefill {
874        return Ok(input.to_owned());
875    }
876
877    let default_value = (params.create_default_value)(app);
878
879    let value = match (input, &default_value) {
880        ("", None) => "",
881        ("", Some(selected)) => selected,
882        (value, _) => value,
883    };
884
885    Ok(value.to_string())
886}
887
888fn tee(maybe_input: Option<&mut impl Read>, outputs: &mut [&mut dyn Write]) -> std::io::Result<()> {
889    let Some(input) = maybe_input else {
890        return Ok(());
891    };
892
893    let mut buf = [0u8; 1024];
894
895    loop {
896        let num_read = input.read(&mut buf)?;
897        if num_read == 0 {
898            break;
899        }
900
901        let buf = &buf[..num_read];
902        for output in &mut *outputs {
903            output.write_all(buf)?;
904        }
905    }
906
907    Ok(())
908}
909
910fn rebase_todo_screen(
911    config: &Arc<Config>,
912    repo: &Rc<Repository>,
913    params: RenderParams,
914    todo: RebaseTodo,
915) -> Res<Screen> {
916    screen::rebase_todo::create(
917        Arc::clone(config),
918        Rc::clone(repo),
919        params,
920        Rc::new(RefCell::new(todo)),
921    )
922}
923
924/// How far back [`App::pick_commit`] lists commits.
925const COMMIT_PICK_LIMIT: usize = 256;
926
927pub(crate) fn root_menu(config: &Config) -> Option<Menu> {
928    if config.general.always_show_help.enabled {
929        Some(Menu::Help)
930    } else {
931        None
932    }
933}
934
935fn write_child_output_to_log(
936    log_rwlock: &mut Arc<RwLock<CmdLogEntry>>,
937    child: &mut Child,
938    status: std::process::ExitStatus,
939) -> Res<()> {
940    let mut log = log_rwlock.write().unwrap();
941
942    let CmdLogEntry::Cmd { args, out: out_log } = log.deref_mut() else {
943        unreachable!("pending_cmd is always CmdLogEntry::Cmd variant");
944    };
945
946    drop(child.stdin.take());
947
948    let mut out_bytes = vec![];
949    log::debug!("Reading stderr");
950
951    child
952        .stderr
953        .take()
954        .unwrap()
955        .read_to_end(&mut out_bytes)
956        .map_err(Error::CouldntReadCmdOutput)?;
957
958    child
959        .stdout
960        .take()
961        .unwrap()
962        .read_to_end(&mut out_bytes)
963        .map_err(Error::CouldntReadCmdOutput)?;
964
965    let out_string = String::from_utf8_lossy(&out_bytes).to_string();
966    *out_log = Some(out_string.into());
967
968    if !status.success() {
969        return Err(Error::CmdBadExit(args.to_string(), status.code()));
970    }
971
972    Ok(())
973}
974
975type DefaultFn = Box<dyn Fn(&App) -> Option<String>>;
976
977pub(crate) struct PromptParams {
978    pub prompt: &'static str,
979    pub create_default_value: DefaultFn,
980    pub hide_menu: bool,
981    /// Put the default value in the prompt for the user to edit, rather than
982    /// keeping it as what an empty answer means.
983    pub prefill: bool,
984    pub history: Option<prompt::HistoryKind>,
985}
986
987impl Default for PromptParams {
988    fn default() -> Self {
989        Self {
990            prompt: "",
991            create_default_value: Box::new(|_| None),
992            hide_menu: true,
993            prefill: false,
994            history: None,
995        }
996    }
997}