Skip to main content

gitu/
app.rs

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