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            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        // The menu a screen imposes is in force from the first key on, not from
181        // the first key that closes a menu.
182        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    /// Give each screen whose rows were still being drawn all of them, once
257    /// they are.
258    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    /// Rebuild every screen, leaving each cursor where it was in the diff rather
274    /// than on the line number it happened to occupy. For a re-render of the
275    /// same content (a renderer feature turned on or off), the rows move but the
276    /// place in the patch does not.
277    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                // The key received changes as shift is pressed: '/' becomes '?'
321                // on a US keyboard, and tab becomes backtab. The modifier is
322                // redundant there, and on the other keys it is the only sign
323                // shift was held.
324                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    /// Prints the screen, and says so, if it fits on the terminal with a row
353    /// to spare for the shell's prompt: less's `--quit-if-one-screen`. Waits
354    /// for the rest of a screen still being drawn while what there is fits.
355    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 the item clicked was already the current item, then try to
439                        // toggle it if it's a section or show it.
440                        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    /// What a screen opened now should render with: the viewport it will get,
505    /// and the features currently selected.
506    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    /// The menu to fall back to once a pending one closes: a screen with its own
524    /// keymap (the interactive rebase todo) imposes it for as long as it is up.
525    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    /// Displays an `Info` message to the CmdLog.
542    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    /// Displays an `Error` message to the CmdLog.
549    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    /// Runs a `Command` and handles its output.
556    /// Will block awaiting its completion.
557    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    /// Runs a `Command` and handles its output asynchronously (if async commands are enabled).
565    /// Will return `Ok(())` if one is already running.
566    pub fn run_cmd_async(&mut self, term: &mut Term, input: &[u8], 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().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    /// Handles any pending_cmd in State without blocking. Returns `true` if a cmd was handled.
610    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"); // No guarantee, but modern tools seem to implement this
631
632        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        // Redirect stderr so we can capture it via `Child::wait_with_output()`
646        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 stdin as `Child::wait_with_output` would
654        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        // A prompt with nothing to say says nothing: the key that opened it
716        // already said what is being answered. Prefilled, the default is the
717        // answer being edited rather than the one an empty answer falls back
718        // to — it is on the line, so naming it again says nothing either.
719        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    /// Show a picker and wait for user to select an item or cancel.
805    ///
806    /// Returns:
807    /// - `Ok(Some(data))` - User selected an item
808    /// - `Ok(None)` - User cancelled (Esc or Ctrl-C)
809    /// - `Err(e)` - An error occurred
810    ///
811    /// # Example
812    /// ```ignore
813    /// let items = vec![
814    ///     PickerItem::new("main", PickerData::Item("main".to_string())),
815    ///     PickerItem::new("develop", PickerData::Item("develop".to_string())),
816    /// ];
817    /// let picker = PickerState::new("Select branch", items, false);
818    ///
819    /// match app.pick(term, picker)? {
820    ///     Some(PickerData::Item(name)) => {
821    ///         // User selected a branch
822    ///         println!("Selected: {}", name);
823    ///     }
824    ///     Some(PickerData::CustomInput(_)) => {
825    ///         // Should not happen when allow_custom_input is false
826    ///     }
827    ///     None => {
828    ///         // User cancelled
829    ///         println!("Cancelled");
830    ///     }
831    /// }
832    /// ```
833    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    /// [`App::pick`], for a picker whose items can be given keys.
843    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    /// Choose a commit by moving around a log view: the screen is pushed with
857    /// its own keymap, and the loop runs until `select_commit` names one or the
858    /// screen is closed.
859    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        // The selected commit has to be found among the rows, so they are all
866        // drawn first.
867        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        // Start on the commit that was already selected, if there was one.
883        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); // The screen was closed: nothing picked.
903            }
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                    // User cancelled - this is not an error
924                    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                // Text input - delegate to text state
963                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
1023/// How far back [`App::pick_commit`] lists commits.
1024const 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    /// Put the default value in the prompt for the user to edit, rather than
1081    /// keeping it as what an empty answer means.
1082    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}